!< StringiFor, definition of `string` type.
module stringifor_string_t
!< StringiFor, definition of `string` type.
use, intrinsic :: iso_fortran_env, only : iostat_eor
use befor64, only : b64_decode, b64_encode
use penf, only : I1P, I2P, I4P, I8P, R4P, R8P, R16P, str

implicit none
private
save
public :: CK
public :: sadjustl_character, sadjustr_character,                                 &
          sindex_string_string, sindex_string_character, sindex_character_string, &
          slen, slen_trim,                                                        &
          srepeat_string_string,                                                  &
          sscan_string_string, sscan_string_character, sscan_character_string,    &
          strim
public :: string

integer, parameter :: CK = selected_char_kind('DEFAULT') !< Default character kind.

type :: string
  !< OOP designed string class.
  private
  character(kind=CK, len=:), allocatable :: raw !< Raw data.
  contains
    ! public methods
    ! builtins replacements
    procedure, pass(self) :: adjustl  => sadjustl                 !< Adjustl replacement.
    procedure, pass(self) :: adjustr  => sadjustr                 !< Adjustr replacement.
    procedure, pass(self) :: count    => scount                   !< Count replacement.
    generic               :: index    => sindex_string_string, &
                                         sindex_string_character  !< Index replacement.
    procedure, pass(self) :: len      => slen                     !< Len replacement.
    procedure, pass(self) :: len_trim => slen_trim                !< Len_trim replacement.
    generic               :: repeat   => srepeat_string_string, &
                                         srepeat_character_string !< Repeat replacement.
    generic               :: scan     => sscan_string_string,    &
                                         sscan_string_character   !< Scan replacement.
    procedure, pass(self) :: trim     => strim                    !< Trim replacement.
    procedure, pass(self) :: verify   => sverify                  !< Verify replacement.
    ! auxiliary methods
    procedure, pass(self) :: basedir          !< Return the base directory name of a string containing a file name.
    procedure, pass(self) :: basename         !< Return the base file name of a string containing a file name.
    procedure, pass(self) :: camelcase        !< Return a string with all words capitalized without spaces.
    procedure, pass(self) :: capitalize       !< Return a string with its first character capitalized and the rest lowercased.
    procedure, pass(self) :: chars            !< Return the raw characters data.
    procedure, pass(self) :: decode           !< Decode string.
    procedure, pass(self) :: encode           !< Encode string.
    procedure, pass(self) :: escape           !< Escape backslashes (or custom escape character).
    procedure, pass(self) :: extension        !< Return the extension of a string containing a file name.
    procedure, pass(self) :: fill             !< Pad string on the left (or right) with zeros (or other char) to fill width.
    procedure, pass(self) :: free             !< Free dynamic memory.
    generic               :: glob =>         &
                             glob_character, &
                             glob_string      !< Glob search, finds all the pathnames matching a given pattern.
    generic               :: insert =>      &
                             insert_string, &
                             insert_character !< Insert substring into string at a specified position.
    generic               :: join =>       &
                             join_strings, &
                             join_characters  !< Return a string that is a join of an array of strings or characters.
    procedure, pass(self) :: lower            !< Return a string with all lowercase characters.
    procedure, pass(self) :: partition        !< Split string at separator and return the 3 parts (before, the separator and after).
    procedure, pass(self) :: read_file        !< Read a file a single string stream.
    procedure, pass(self) :: read_line        !< Read line (record) from a connected unit.
    procedure, pass(self) :: read_lines       !< Read (all) lines (records) from a connected unit as a single ascii stream.
    procedure, pass(self) :: replace          !< Return a string with all occurrences of substring old replaced by new.
    procedure, pass(self) :: reverse          !< Return a reversed string.
    procedure, pass(self) :: search           !< Search for *tagged* record into string.
    procedure, pass(self) :: slice            !< Return the raw characters data sliced.
    procedure, pass(self) :: snakecase        !< Return a string with all words lowercase separated by "_".
    procedure, pass(self) :: split            !< Return a list of substring in the string, using sep as the delimiter string.
    procedure, pass(self) :: split_chunked    !< Return a list of substring in the string, using sep as the delimiter string.
    procedure, pass(self) :: startcase        !< Return a string with all words capitalized, e.g. title case.
    procedure, pass(self) :: strip            !< Return a string with the leading and trailing characters removed.
    procedure, pass(self) :: swapcase         !< Return a string with uppercase chars converted to lowercase and vice versa.
    procedure, pass(self) :: tempname         !< Return a safe temporary name suitable for temporary file or directories.
    generic               :: to_number =>   &
                             to_integer_I1P,&
                             to_integer_I2P,&
                             to_integer_I4P,&
                             to_integer_I8P,&
                             to_real_R4P,   &
#ifdef _R16P_SUPPORTED
                             to_real_R8P,   &
                             to_real_R16P     !< Cast string to number.
#else
                             to_real_R8P      !< Cast string to number.
#endif
    procedure, pass(self) :: unescape         !< Unescape double backslashes (or custom escaped character).
    procedure, pass(self) :: unique           !< Reduce to one (unique) multiple occurrences of a substring into a string.
    procedure, pass(self) :: upper            !< Return a string with all uppercase characters.
    procedure, pass(self) :: write_file       !< Write a single string stream into file.
    procedure, pass(self) :: write_line       !< Write line (record) to a connected unit.
    procedure, pass(self) :: write_lines      !< Write lines (records) to a connected unit.
    ! inquire methods
    procedure, pass(self) :: end_with     !< Return true if a string ends with a specified suffix.
    procedure, pass(self) :: is_allocated !< Return true if the string is allocated.
    procedure, pass(self) :: is_digit     !< Return true if all characters in the string are digits.
    procedure, pass(self) :: is_integer   !< Return true if the string contains an integer.
    procedure, pass(self) :: is_lower     !< Return true if all characters in the string are lowercase.
    procedure, pass(self) :: is_number    !< Return true if the string contains a number (real or integer).
    procedure, pass(self) :: is_real      !< Return true if the string contains an real.
    procedure, pass(self) :: is_upper     !< Return true if all characters in the string are uppercase.
    procedure, pass(self) :: start_with   !< Return true if a string starts with a specified prefix.
    ! operators
    generic :: assignment(=) => string_assign_string,      &
                                string_assign_character,   &
                                string_assign_integer_I1P, &
                                string_assign_integer_I2P, &
                                string_assign_integer_I4P, &
                                string_assign_integer_I8P, &
                                string_assign_real_R4P,    &
#ifdef _R16P_SUPPORTED
                                string_assign_real_R8P,    &
                                string_assign_real_R16P             !< Assignment operator overloading.
#else
                                string_assign_real_R8P              !< Assignment operator overloading.
#endif
    generic :: operator(//) => string_concat_string,    &
                               string_concat_character, &
                               character_concat_string              !< Concatenation operator overloading.
    generic :: operator(.cat.) => string_concat_string_string,    &
                                  string_concat_character_string, &
                                  character_concat_string_string    !< Concatenation operator (string output) overloading.
    generic :: operator(==) => string_eq_string,    &
                               string_eq_character, &
                               character_eq_string                  !< Equal operator overloading.
    generic :: operator(/=) => string_ne_string,    &
                               string_ne_character, &
                               character_ne_string                  !< Not equal operator overloading.
    generic :: operator(<) => string_lt_string,    &
                              string_lt_character, &
                              character_lt_string                   !< Lower than operator overloading.
    generic :: operator(<=) => string_le_string,    &
                               string_le_character, &
                               character_le_string                  !< Lower equal than operator overloading.
    generic :: operator(>=) => string_ge_string,    &
                               string_ge_character, &
                               character_ge_string                  !< Greater equal than operator overloading.
    generic :: operator(>) => string_gt_string,    &
                              string_gt_character, &
                              character_gt_string                   !< Greater than operator overloading.
    ! IO
#ifndef __GFORTRAN__
    generic :: read(formatted) => read_formatted       !< Formatted input.
    generic :: write(formatted) => write_formatted     !< Formatted output.
    generic :: read(unformatted) => read_unformatted   !< Unformatted input.
    generic :: write(unformatted) => write_unformatted !< Unformatted output.
#endif
    ! private methods
    ! builtins replacements
    procedure, private, pass(self) :: sindex_string_string     !< Index replacement.
    procedure, private, pass(self) :: sindex_string_character  !< Index replacement.
    procedure, private, pass(self) :: srepeat_string_string    !< Repeat replacement.
    procedure, private, nopass     :: srepeat_character_string !< Repeat replacement.
    procedure, private, pass(self) :: sscan_string_string      !< Scan replacement.
    procedure, private, pass(self) :: sscan_string_character   !< Scan replacement.
    ! auxiliary methods
    procedure, private, pass(self) :: glob_character   !< Glob search (character output).
    procedure, private, pass(self) :: glob_string      !< Glob search (string output).
    procedure, private, pass(self) :: insert_string    !< Insert substring into string at a specified position.
    procedure, private, pass(self) :: insert_character !< Insert substring into string at a specified position.
    procedure, private, pass(self) :: join_strings     !< Return join string of an array of strings.
    procedure, private, pass(self) :: join_characters  !< Return join string of an array of characters.
    procedure, private, pass(self) :: to_integer_I1P   !< Cast string to integer.
    procedure, private, pass(self) :: to_integer_I2P   !< Cast string to integer.
    procedure, private, pass(self) :: to_integer_I4P   !< Cast string to integer.
    procedure, private, pass(self) :: to_integer_I8P   !< Cast string to integer.
    procedure, private, pass(self) :: to_real_R4P      !< Cast string to real.
    procedure, private, pass(self) :: to_real_R8P      !< Cast string to real.
    procedure, private, pass(self) :: to_real_R16P     !< Cast string to real.
    ! assignments
    procedure, private, pass(lhs) :: string_assign_string      !< Assignment operator from string input.
    procedure, private, pass(lhs) :: string_assign_character   !< Assignment operator from character input.
    procedure, private, pass(lhs) :: string_assign_integer_I1P !< Assignment operator from integer input.
    procedure, private, pass(lhs) :: string_assign_integer_I2P !< Assignment operator from integer input.
    procedure, private, pass(lhs) :: string_assign_integer_I4P !< Assignment operator from integer input.
    procedure, private, pass(lhs) :: string_assign_integer_I8P !< Assignment operator from integer input.
    procedure, private, pass(lhs) :: string_assign_real_R4P    !< Assignment operator from real input.
    procedure, private, pass(lhs) :: string_assign_real_R8P    !< Assignment operator from real input.
    procedure, private, pass(lhs) :: string_assign_real_R16P   !< Assignment operator from real input.
    ! concatenation operators
    procedure, private, pass(lhs) :: string_concat_string           !< Concatenation with string.
    procedure, private, pass(lhs) :: string_concat_character        !< Concatenation with character.
    procedure, private, pass(rhs) :: character_concat_string        !< Concatenation with character (inverted).
    procedure, private, pass(lhs) :: string_concat_string_string    !< Concatenation with string (string output).
    procedure, private, pass(lhs) :: string_concat_character_string !< Concatenation with character (string output).
    procedure, private, pass(rhs) :: character_concat_string_string !< Concatenation with character (inverted, string output).
    ! logical operators
    procedure, private, pass(lhs) :: string_eq_string    !< Equal to string logical operator.
    procedure, private, pass(lhs) :: string_eq_character !< Equal to character logical operator.
    procedure, private, pass(rhs) :: character_eq_string !< Equal to character (inverted) logical operator.
    procedure, private, pass(lhs) :: string_ne_string    !< Not equal to string logical operator.
    procedure, private, pass(lhs) :: string_ne_character !< Not equal to character logical operator.
    procedure, private, pass(rhs) :: character_ne_string !< Not equal to character (inverted) logical operator.
    procedure, private, pass(lhs) :: string_lt_string    !< Lower than to string logical operator.
    procedure, private, pass(lhs) :: string_lt_character !< Lower than to character logical operator.
    procedure, private, pass(rhs) :: character_lt_string !< Lower than to character (inverted) logical operator.
    procedure, private, pass(lhs) :: string_le_string    !< Lower equal than to string logical operator.
    procedure, private, pass(lhs) :: string_le_character !< Lower equal than to character logical operator.
    procedure, private, pass(rhs) :: character_le_string !< Lower equal than to character (inverted) logical operator.
    procedure, private, pass(lhs) :: string_ge_string    !< Greater equal than to string logical operator.
    procedure, private, pass(lhs) :: string_ge_character !< Greater equal than to character logical operator.
    procedure, private, pass(rhs) :: character_ge_string !< Greater equal than to character (inverted) logical operator.
    procedure, private, pass(lhs) :: string_gt_string    !< Greater than to string logical operator.
    procedure, private, pass(lhs) :: string_gt_character !< Greater than to character logical operator.
    procedure, private, pass(rhs) :: character_gt_string !< Greater than to character (inverted) logical operator.
    ! IO
    procedure, private, pass(dtv) :: read_formatted                !< Formatted input.
    procedure, private, pass(dtv) :: read_delimited                !< Read a delimited input.
    procedure, private, pass(dtv) :: read_undelimited              !< Read an undelimited input.
    procedure, private, pass(dtv) :: read_undelimited_listdirected !< Read an undelimited list directed input.
    procedure, private, pass(dtv) :: write_formatted               !< Formatted output.
    procedure, private, pass(dtv) :: read_unformatted              !< Unformatted input.
    procedure, private, pass(dtv) :: write_unformatted             !< Unformatted output.
    ! miscellanea
    procedure, private, pass(self) :: replace_one_occurrence !< Replace the first occurrence of substring old by new.
endtype string

! internal parameters
character(kind=CK, len=26), parameter :: UPPER_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' !< Upper case alphabet.
character(kind=CK, len=26), parameter :: LOWER_ALPHABET = 'abcdefghijklmnopqrstuvwxyz' !< Lower case alphabet.
character(kind=CK, len=1),  parameter :: SPACE          = ' '                          !< Space character.
character(kind=CK, len=1),  parameter :: TAB            = achar(9)                     !< Tab character.
character(kind=CK, len=1),  parameter :: UIX_DIR_SEP    = char(47)                     !< Unix/Linux directories separator (/).
character(kind=CK, len=1),  parameter :: BACKSLASH      = char(92)                     !< Backslash character.

contains
  ! public methods

  ! builtins replacements
  elemental function sadjustl(self) result(adjusted)
  !< Left adjust a string by removing leading spaces.
  class(string), intent(in) :: self     !< The string.
  type(string)              :: adjusted !< Adjusted string.

  adjusted = self
  if (allocated(adjusted%raw)) adjusted%raw = adjustl(adjusted%raw)
  endfunction sadjustl

  pure function sadjustl_character(self) result(adjusted)
  !< Left adjust a string by removing leading spaces (character output).
  class(string), intent(in)              :: self     !< The string.
  character(kind=CK, len=:), allocatable :: adjusted !< Adjusted string.

  if (allocated(self%raw)) adjusted = adjustl(self%raw)
  endfunction sadjustl_character

  elemental function sadjustr(self) result(adjusted)
  !< Right adjust a string by removing leading spaces.
  class(string), intent(in) :: self     !< The string.
  type(string)              :: adjusted !< Adjusted string.

  adjusted = self
  if (allocated(adjusted%raw)) adjusted%raw = adjustr(adjusted%raw)
  endfunction sadjustr

  pure function sadjustr_character(self) result(adjusted)
  !< Right adjust a string by removing leading spaces (character output).
  class(string), intent(in)              :: self     !< The string.
  character(kind=CK, len=:), allocatable :: adjusted !< Adjusted string.

  if (allocated(self%raw)) adjusted = adjustr(self%raw)
  endfunction sadjustr_character

  elemental function scount(self, substring, ignore_isolated) result(No)
  !< Count the number of occurences of a substring into a string.
  !<
  !< @note If `ignore_isolated` is set to true the eventual "isolated" occurences are ignored: an isolated occurrences are those
  !< occurrences happening at the start of string (thus not having a left companion) or at the end of the string (thus not having a
  !< right companion).
  class(string), intent(in)              :: self             !< The string.
  character(*),  intent(in)              :: substring        !< Substring.
  logical,       intent(in), optional    :: ignore_isolated  !< Ignore "isolated" occurrences.
  integer                                :: No               !< Number of occurrences.
  logical                                :: ignore_isolated_ !< Ignore "isolated" occurrences, local variable.
  integer                                :: c1               !< Counter.
  integer                                :: c2               !< Counter.
#ifdef __GFORTRAN__
  character(kind=CK, len=:), allocatable :: temporary        !< Temporary storage, workaround for GNU bug.
#endif

  No = 0
  if (allocated(self%raw)) then
    if (len(substring)>len(self%raw)) return
    ignore_isolated_ = .false. ; if (present(ignore_isolated)) ignore_isolated_ = ignore_isolated
#ifdef __GFORTRAN__
    temporary = self%raw
#endif
    c1 = 1
    do
#ifdef __GFORTRAN__
      c2 = index(string=temporary(c1:), substring=substring)
#else
      c2 = index(string=self%raw(c1:), substring=substring)
#endif
      if (c2==0) return
      if (.not.(ignore_isolated_.and.(c1==1.or.c1+c2-1==len(self%raw)-len(substring)+1))) then
        No = No + 1
      endif
      c1 = c1 + c2 - 1 + len(substring)
    enddo
  endif
  endfunction scount

  elemental function sindex_string_string(self, substring, back) result(i)
  !< Return the position of the start of the first occurrence of string `substring` as a substring in `string`, counting from one.
  !< If `substring` is not present in `string`, zero is returned. If the back argument is present and true, the return value is
  !< the start of the last occurrence rather than the first.
  class(string), intent(in)           :: self      !< The string.
  type(string),  intent(in)           :: substring !< Searched substring.
  logical,       intent(in), optional :: back      !< Start of the last occurrence rather than the first.
  integer                             :: i         !< Result of the search.

  if (allocated(self%raw)) then
    i = index(string=self%raw, substring=substring%raw, back=back)
  else
    i = 0
  endif
  endfunction sindex_string_string

  elemental function sindex_string_character(self, substring, back) result(i)
  !< Return the position of the start of the first occurrence of string `substring` as a substring in `string`, counting from one.
  !< If `substring` is not present in `string`, zero is returned. If the back argument is present and true, the return value is
  !< the start of the last occurrence rather than the first.
  class(string),             intent(in)           :: self      !< The string.
  character(kind=CK, len=*), intent(in)           :: substring !< Searched substring.
  logical,                   intent(in), optional :: back      !< Start of the last occurrence rather than the first.
  integer                                         :: i         !< Result of the search.

  if (allocated(self%raw)) then
    i = index(string=self%raw, substring=substring, back=back)
  else
    i = 0
  endif
  endfunction sindex_string_character

  elemental function sindex_character_string(string_, substring, back) result(i)
  !< Return the position of the start of the first occurrence of string `substring` as a substring in `string`, counting from one.
  !< If `substring` is not present in `string`, zero is returned. If the back argument is present and true, the return value is
  !< the start of the last occurrence rather than the first.
  character(kind=CK, len=*), intent(in)           :: string_   !< The string.
  type(string),              intent(in)           :: substring !< Searched substring.
  logical,                   intent(in), optional :: back      !< Start of the last occurrence rather than the first.
  integer                                         :: i         !< Result of the search.

  if (allocated(substring%raw)) then
    i = index(string=string_, substring=substring%raw, back=back)
  else
    i = 0
  endif
  endfunction sindex_character_string

  elemental function slen(self) result(l)
  !< Return the length of a string.
  class(string), intent(in) :: self !< The string.
  integer                   :: l    !< String length.

  if (allocated(self%raw)) then
    l = len(string=self%raw)
  else
    l = 0
  endif
  endfunction slen

  elemental function slen_trim(self) result(l)
  !< Return the length of a string, ignoring any trailing blanks.
  class(string), intent(in) :: self !< The string.
  integer                   :: l    !< String length.

  if (allocated(self%raw)) then
    l = len_trim(string=self%raw)
  else
    l = 0
  endif
  endfunction slen_trim

  elemental function srepeat_string_string(self, ncopies) result(repeated)
  !< Concatenates several copies of an input string.
  class(string), intent(in) :: self     !< String to be repeated.
  integer,       intent(in) :: ncopies  !< Number of string copies.
  type(string)              :: repeated !< Repeated string.

  repeated%raw = repeat(string=self%raw, ncopies=ncopies)
  endfunction srepeat_string_string

  elemental function srepeat_character_string(rstring, ncopies) result(repeated)
  !< Concatenates several copies of an input string.
  character(kind=CK, len=*), intent(in) :: rstring  !< String to be repeated.
  integer,                   intent(in) :: ncopies  !< Number of string copies.
  type(string)                          :: repeated !< Repeated string.

  repeated%raw = repeat(string=rstring, ncopies=ncopies)
  endfunction srepeat_character_string

  elemental function sscan_string_string(self, set, back) result(i)
  !< Return the leftmost (if `back` is either absent or equals false, otherwise the rightmost) character of string that is in `set`.
  class(string), intent(in)           :: self  !< The string.
  type(string),  intent(in)           :: set   !< Searched set.
  logical,       intent(in), optional :: back  !< Start of the last occurrence rather than the first.
  integer                             :: i     !< Result of the search.

  if (allocated(self%raw).and.allocated(set%raw)) then
    i = scan(string=self%raw, set=set%raw, back=back)
  else
    i = 0
  endif
  endfunction sscan_string_string

  elemental function sscan_string_character(self, set, back) result(i)
  !< Return the leftmost (if `back` is either absent or equals false, otherwise the rightmost) character of string that is in `set`.
  class(string),             intent(in)           :: self  !< The string.
  character(kind=CK, len=*), intent(in)           :: set   !< Searched set.
  logical,                   intent(in), optional :: back  !< Start of the last occurrence rather than the first.
  integer                                         :: i     !< Result of the search.

  if (allocated(self%raw)) then
    i = scan(string=self%raw, set=set, back=back)
  else
    i = 0
  endif
  endfunction sscan_string_character

  elemental function sscan_character_string(sstring, set, back) result(i)
  !< Return the leftmost (if `back` is either absent or equals false, otherwise the rightmost) character of string that is in `set`.
  character(kind=CK, len=*), intent(in)           :: sstring !< The string.
  type(string),              intent(in)           :: set     !< Searched set.
  logical,                   intent(in), optional :: back    !< Start of the last occurrence rather than the first.
  integer                                         :: i       !< Result of the search.

  if (allocated(set%raw)) then
    i = scan(string=sstring, set=set%raw, back=back)
  else
    i = 0
  endif
  endfunction sscan_character_string

  elemental function strim(self) result(trimmed)
  !< Remove leading spaces.
  class(string), intent(in) :: self    !< The string.
  type(string)              :: trimmed !< Trimmed string.

  trimmed = self
  if (allocated(trimmed%raw)) trimmed%raw = trim(trimmed%raw)
  endfunction strim

  elemental function sverify(self, set, back) result(i)
  !< Return the leftmost (if `back` is either absent or equals false, otherwise the rightmost) character of string that is not
  !< in `set`. If all characters of `string` are found in `set`, the result is zero.
  class(string),             intent(in)           :: self  !< The string.
  character(kind=CK, len=*), intent(in)           :: set   !< Searched set.
  logical,                   intent(in), optional :: back  !< Start of the last occurrence rather than the first.
  integer                                         :: i     !< Result of the search.

  if (allocated(self%raw)) then
    i = verify(string=self%raw, set=set, back=back)
  else
    i = 0
  endif
  endfunction sverify

  ! auxiliary methods
  elemental function basedir(self, sep)
  !< Return the base directory name of a string containing a file name.
  !<
  !<### Example
  !<
  !<```fortran
  !< type(string) :: astring
  !< astring = '/bar/foo.tar.bz2'
  !< print '(A)', astring%basedir()//'' ! print "/bar"
  !<```
  class(string),             intent(in)           :: self    !< The string.
  character(kind=CK, len=*), intent(in), optional :: sep     !< Directory separator.
  type(string)                                    :: basedir !< Base directory name.
  character(kind=CK, len=:), allocatable          :: sep_    !< Separator, default value.
  integer                                         :: pos     !< Character position.

  if (allocated(self%raw)) then
    sep_ = UIX_DIR_SEP ; if (present(sep)) sep_ = sep
    basedir = self
    pos = index(self%raw, sep_, back=.true.)
    if (pos>0) basedir%raw = self%raw(1:pos-1)
  endif
  endfunction basedir

  elemental function basename(self, sep, extension, strip_last_extension)
  !< Return the base file name of a string containing a file name.
  !<
  !< Optionally, the extension is also stripped if provided or the last one if required, e.g.
  !<
  !<### Example
  !<
  !<```fortran
  !< type(string) :: astring
  !< astring = 'bar/foo.tar.bz2'
  !< print '(A)', astring%basename(extension='.tar.bz2')//''        ! print "foo"
  !< print '(A)', astring%basename(strip_last_extension=.true.)//'' ! print "foo.tar"
  !<```
  class(string),             intent(in)           :: self                 !< The string.
  character(kind=CK, len=*), intent(in), optional :: sep                  !< Directory separator.
  character(kind=CK, len=*), intent(in), optional :: extension            !< File extension.
  logical,                   intent(in), optional :: strip_last_extension !< Flag to enable the stripping of last extension.
  type(string)                                    :: basename             !< Base file name.
  character(kind=CK, len=:), allocatable          :: sep_                 !< Separator, default value.
  integer                                         :: pos                  !< Character position.
#ifdef __GFORTRAN__
  character(kind=CK, len=:), allocatable          :: temporary            !< Temporary storage, workaround for GNU bug.
#endif

  if (allocated(self%raw)) then
    sep_ = UIX_DIR_SEP ; if (present(sep)) sep_ = sep
    basename = self
#ifdef __GFORTRAN__
    temporary = basename%raw
    pos = index(temporary, sep_, back=.true.)
    if (pos>0) basename%raw = temporary(pos+1:)
#else
    pos = index(basename%raw, sep_, back=.true.)
    if (pos>0) basename%raw = self%raw(pos+1:)
#endif
    if (present(extension)) then
#ifdef __GFORTRAN__
      temporary = basename%raw
      pos = index(temporary, extension, back=.true.)
      if (pos>0) basename%raw = temporary(1:pos-1)
#else
      pos = index(basename%raw, extension, back=.true.)
      if (pos>0) basename%raw = basename%raw(1:pos-1)
#endif
    elseif (present(strip_last_extension)) then
      if (strip_last_extension) then
#ifdef __GFORTRAN__
        temporary = basename%raw
        pos = index(temporary, '.', back=.true.)
        basename%raw = temporary(1:pos-1)
#else
        pos = index(basename%raw, '.', back=.true.)
        basename%raw = basename%raw(1:pos-1)
#endif
      endif
    endif
  endif
  endfunction basename

  elemental function camelcase(self, sep)
  !< Return a string with all words capitalized without spaces.
  !<
  !< @note Multiple subsequent separators are collapsed to one occurence.
  !<
  !<### Example
  !<
  !<```fortran
  !< type(string) :: astring
  !< astring = 'caMeL caSe var'
  !< print '(A)', astring%camelcase()//'' ! print "CamelCaseVar"
  class(string),             intent(in)           :: self      !< The string.
  character(kind=CK, len=*), intent(in), optional :: sep       !< Separator.
  type(string)                                    :: camelcase !< Camel case string.
  type(string), allocatable                       :: tokens(:) !< String tokens.

  if (allocated(self%raw)) then
    call self%split(tokens=tokens, sep=sep)
    tokens = tokens%capitalize()
    camelcase = camelcase%join(array=tokens)
  endif
  endfunction camelcase

  elemental function capitalize(self) result(capitalized)
  !< Return a string with its first character capitalized and the rest lowercased.
  class(string), intent(in) :: self        !< The string.
  type(string)              :: capitalized !< Upper case string.
  integer                   :: c           !< Character counter.

  if (allocated(self%raw)) then
    capitalized = self%lower()
    c = index(LOWER_ALPHABET, capitalized%raw(1:1))
    if (c>0) capitalized%raw(1:1) = UPPER_ALPHABET(c:c)
  endif
  endfunction capitalize

  pure function chars(self) result(raw)
  !< Return the raw characters data.
  class(string), intent(in)              :: self !< The string.
  character(kind=CK, len=:), allocatable :: raw  !< Raw characters data.

  if (allocated(self%raw)) then
    raw = self%raw
  else
    raw = ''
  endif
  endfunction chars

  elemental function decode(self, codec) result(decoded)
  !< Return a string decoded accordingly the codec.
  !<
  !< @note Only BASE64 codec is currently available.
  !<
  !<### Example
  !<
  !<```fortran
  !< type(string) :: astring
  !< astring = 'SG93IGFyZSB5b3U/'
  !< print '(A)', astring%decode(codec='base64')//'' ! print "How are you?"
  class(string),             intent(in) :: self    !< The string.
  character(kind=CK, len=*), intent(in) :: codec   !< Encoding codec.
  type(string)                          :: decoded !< Decoded string.
  type(string)                          :: codec_u !< Encoding codec in upper case string.

  if (allocated(self%raw)) then
    decoded = self
    codec_u = codec
    select case(codec_u%upper()//'')
    case('BASE64')
      call b64_decode(code=self%raw, s=decoded%raw)
    endselect
    decoded = decoded%strip(remove_nulls=.true.)
  endif
  endfunction decode

  elemental function encode(self, codec) result(encoded)
  !< Return a string encoded accordingly the codec.
  !<
  !< @note Only BASE64 codec is currently available.
  !<
  !<### Example
  !<
  !<```fortran
  !< type(string) :: astring
  !< astring = 'How are you?'
  !< print '(A)', astring%encode(codec='base64')//'' ! print "SG93IGFyZSB5b3U/"
  class(string),             intent(in) :: self    !< The string.
  character(kind=CK, len=*), intent(in) :: codec   !< Encoding codec.
  type(string)                          :: encoded !< Encoded string.

  if (allocated(self%raw)) then
    encoded = codec
    select case(encoded%upper()//'')
    case('BASE64')
      call b64_encode(s=self%raw, code=encoded%raw)
    endselect
  endif
  endfunction encode

  elemental function escape(self, to_escape, esc) result(escaped)
  !< Escape backslashes (or custom escape character).
  class(string),             intent(in)           :: self      !< The string.
  character(kind=CK, len=1), intent(in)           :: to_escape !< Character to be escaped.
  character(kind=CK, len=*), intent(in), optional :: esc       !< Character used to escape.
  type(string)                                    :: escaped   !< Escaped string.
  character(kind=CK, len=:), allocatable          :: esc_      !< Character to escape, local variable.
  integer                                         :: c         !< Character counter.

  if (allocated(self%raw)) then
    esc_ = BACKSLASH ; if (present(esc)) esc_ = esc
    escaped%raw = ''
    do c=1, len(self%raw)
      if (self%raw(c:c)==to_escape) then
        escaped%raw = escaped%raw//esc_//to_escape
      else
        escaped%raw = escaped%raw//self%raw(c:c)
      endif
    enddo
  endif
  endfunction escape

  elemental function extension(self)
  !< Return the extension of a string containing a file name.
  class(string), intent(in)              :: self      !< The string.
  type(string)                           :: extension !< Extension file name.
  integer                                :: pos       !< Character position.
#ifdef __GFORTRAN__
  character(kind=CK, len=:), allocatable :: temporary !< Temporary storage, workaround for GNU bug.
#endif

  if (allocated(self%raw)) then
    extension = ''
    pos = index(self%raw, '.', back=.true.)
#ifdef __GFORTRAN__
    temporary = self%raw
    if (pos>0) extension%raw = temporary(pos:)
#else
    if (pos>0) extension%raw = self%raw(pos:)
#endif
  endif
  endfunction extension

  elemental function fill(self, width, right, filling_char) result(filled)
  !< Pad string on the left (or right) with zeros (or other char) to fill width.
  class(string),             intent(in)           :: self          !< The string.
  integer,                   intent(in)           :: width         !< Final width of filled string.
  logical,                   intent(in), optional :: right         !< Fill on the right instead of left.
  character(kind=CK, len=1), intent(in), optional :: filling_char  !< Filling character (default "0").
  type(string)                                    :: filled        !< Filled string.
  logical                                         :: right_        !< Fill on the right instead of left, local variable.
  character(kind=CK, len=1)                       :: filling_char_ !< Filling character (default "0"), local variable.

  if (allocated(self%raw)) then
    if (width>len(self%raw)) then
      right_ = .false. ; if (present(right)) right_ = right
      filling_char_ = '0' ; if (present(filling_char)) filling_char_ = filling_char
      if (.not.right_) then
        filled%raw = repeat(filling_char_, width-len(self%raw))//self%raw
      else
        filled%raw = self%raw//repeat(filling_char_, width-len(self%raw))
      endif
    endif
  endif
  endfunction fill

  elemental subroutine free(self)
  !< Free dynamic memory.
  class(string), intent(inout) :: self !< The string.

  if (allocated(self%raw)) deallocate(self%raw)
  endsubroutine free

  subroutine glob_character(self, pattern, list)
  !< Glob search (character output), finds all the pathnames matching a given pattern according to the rules used by the Unix shell.
  !<
  !< @note Method not portable: works only on Unix/GNU Linux OS.
  class(string),                 intent(in)  :: self           !< The string.
  character(*),                  intent(in)  :: pattern        !< Given pattern.
  character(len=:), allocatable, intent(out) :: list(:)        !< List of matching pathnames.
  type(string), allocatable                  :: list_(:)       !< List of matching pathnames.
  integer(I4P)                               :: max_len        !< Maximum length.
  integer(I4P)                               :: matches_number !< Matches number.
  integer(I4P)                               :: m              !< Counter.

  call self%glob(pattern=pattern, list=list_)
  if (allocated(list_)) then
    matches_number = size(list_, dim=1)
    max_len = 0
    do m=1, matches_number
      max_len = max(max_len, list_(m)%len())
    enddo
    allocate(character(max_len) :: list(1:matches_number))
    do m=1, matches_number
      list(m) = list_(m)%chars()
    enddo
  endif
  endsubroutine glob_character

  subroutine glob_string(self, pattern, list)
  !< Glob search (string output), finds all the pathnames matching a given pattern according to the rules used by the Unix shell.
  !<
  !< @note Method not portable: works only on Unix/GNU Linux OS.
  class(string),             intent(in)  :: self     !< The string.
  character(*),              intent(in)  :: pattern  !< Given pattern.
  type(string), allocatable, intent(out) :: list(:)  !< List of matching pathnames.
  type(string)                           :: tempfile !< Safe temporary file.
  character(len=:), allocatable          :: tempname !< Safe temporary name.
  integer(I4P)                           :: tempunit !< Unit of temporary file.

  tempname = self%tempname()
  call execute_command_line('ls -1 '//trim(adjustl(pattern))//' > '//tempname)
  call tempfile%read_file(file=tempname)
  call tempfile%split(sep=new_line('a'), tokens=list)
  open(newunit=tempunit, file=tempname)
  close(unit=tempunit, status='delete')
  endsubroutine glob_string

  elemental function insert_character(self, substring, pos) result(inserted)
  !< Insert substring into string at a specified position.
  class(string),    intent(in) :: self      !< The string.
  character(len=*), intent(in) :: substring !< Substring.
  integer,          intent(in) :: pos       !< Position from which insert substring.
  type(string)                 :: inserted  !< Inserted string.
  integer                      :: safepos   !< Safe position from which insert substring.

  if (allocated(self%raw)) then
    inserted = self
    safepos = min(max(1, pos), len(self%raw))
    if (safepos==1) then
      inserted%raw = substring//self%raw
    elseif (safepos==len(self%raw)) then
      inserted%raw = self%raw//substring
    else
      inserted%raw = self%raw(1:safepos-1)//substring//self%raw(safepos:)
    endif
  else
    inserted%raw = substring
  endif
  endfunction insert_character

  elemental function insert_string(self, substring, pos) result(inserted)
  !< Insert substring into string at a specified position.
  class(string), intent(in) :: self      !< The string.
  type(string),  intent(in) :: substring !< Substring.
  integer,       intent(in) :: pos       !< Position from which insert substring.
  type(string)              :: inserted  !< Inserted string.
  integer                   :: safepos   !< Safe position from which insert substring.

  if (allocated(self%raw)) then
    inserted = self
    if (allocated(substring%raw)) then
      safepos = min(max(1, pos), len(self%raw))
      if (safepos==1) then
        inserted%raw = substring%raw//self%raw
      elseif (safepos==len(self%raw)) then
        inserted%raw = self%raw//substring%raw
      else
        inserted%raw = self%raw(1:safepos-1)//substring%raw//self%raw(safepos:)
      endif
    endif
  else
    if (allocated(substring%raw)) inserted%raw = substring%raw
  endif
  endfunction insert_string

  pure function join_strings(self, array, sep) result(join)
  !< Return a string that is a join of an array of strings.
  !<
  !< The join-separator is set equals to self if self has a value or it is set to a null string ''. This value can be overridden
  !< passing a custom separator.
  class(string),             intent(in)           :: self      !< The string.
  type(string),              intent(in)           :: array(1:) !< Array to be joined.
  character(kind=CK, len=*), intent(in), optional :: sep       !< Separator.
  type(string)                                    :: join      !< The join of array.
  character(kind=CK, len=:), allocatable          :: sep_      !< Separator, default value.
  integer                                         :: a         !< Counter.

  if (allocated(self%raw)) then
    sep_ = self%raw
  else
    sep_ = ''
  endif
  if (present(sep)) sep_ = sep
  join = ''
  do a=2, size(array, dim=1)
    if (allocated(array(a)%raw)) join%raw = join%raw//sep_//array(a)%raw
  enddo
  if (allocated(array(1)%raw)) then
    join%raw = array(1)%raw//join%raw
  else
    join%raw = join%raw(len(sep_)+1:len(join%raw))
  endif
  endfunction join_strings

  pure function join_characters(self, array, sep) result(join)
  !< Return a string that is a join of an array of characters.
  !<
  !< The join-separator is set equals to self if self has a value or it is set to a null string ''. This value can be overridden
  !< passing a custom separator.
  class(string),             intent(in)           :: self      !< The string.
  character(kind=CK, len=*), intent(in)           :: array(1:) !< Array to be joined.
  character(kind=CK, len=*), intent(in), optional :: sep       !< Separator.
  type(string)                                    :: join      !< The join of array.
  character(kind=CK, len=:), allocatable          :: sep_      !< Separator, default value.
  integer                                         :: a         !< Counter.

  if (allocated(self%raw)) then
    sep_ = self%raw
  else
    sep_ = ''
  endif
  if (present(sep)) sep_ = sep
  join = ''
  do a=2, size(array, dim=1)
    if (array(a)/='') join%raw = join%raw//sep_//array(a)
  enddo
  if (array(1)/='') then
    join%raw = array(1)//join%raw
  else
    join%raw = join%raw(len(sep_)+1:len(join%raw))
  endif
  endfunction join_characters

  elemental function lower(self)
  !< Return a string with all lowercase characters.
  class(string), intent(in) :: self  !< The string.
  type(string)              :: lower !< Upper case string.
  integer                   :: n1    !< Characters counter.
  integer                   :: n2    !< Characters counter.

  if (allocated(self%raw)) then
    lower = self
    do n1=1, len(self%raw)
      n2 = index(UPPER_ALPHABET, self%raw(n1:n1))
      if (n2>0) lower%raw(n1:n1) = LOWER_ALPHABET(n2:n2)
    enddo
  endif
  endfunction lower

  pure function partition(self, sep) result(partitions)
  !< Split string at separator and return the 3 parts (before, the separator and after).
  class(string),             intent(in)           :: self            !< The string.
  character(kind=CK, len=*), intent(in), optional :: sep             !< Separator.
  type(string)                                    :: partitions(1:3) !< Partions: before the separator, the separator itsels and
                                                                     !< after the separator.
  character(kind=CK, len=:), allocatable          :: sep_            !< Separator, default value.
  integer                                         :: c               !< Character counter.
#ifdef __GFORTRAN__
  character(kind=CK, len=:), allocatable          :: temporary       !< Temporary storage, workaround for GNU bug.
#endif

  if (allocated(self%raw)) then
    sep_ = SPACE ; if (present(sep)) sep_ = sep

    partitions(1) = self
    partitions(2) = sep_
    partitions(3) = ''
    if (len(sep_)>=len(self%raw)) return
    c = index(self%raw, sep_)
    if (c>0) then
#ifdef __GFORTRAN__
      temporary = self%raw
      partitions(1)%raw = temporary(1:c-1)
      partitions(2)%raw = temporary(c:c+len(sep_)-1)
      partitions(3)%raw = temporary(c+len(sep_):)
#else
      partitions(1)%raw = self%raw(1:c-1)
      partitions(2)%raw = self%raw(c:c+len(sep_)-1)
      partitions(3)%raw = self%raw(c+len(sep_):)
#endif
    endif
  endif
  endfunction partition

  subroutine read_file(self, file, is_fast, form, iostat, iomsg)
  !< Read a file as a single string stream.
  !<
  !< @note All the lines are stored into the string self as a single ascii stream. Each line (record) is separated by a `new_line`
  !< character.
  !<
  !< @note For unformatted read only `access='stream'` is supported with new_line as line terminator.
  !<
  !< @note *Fast* file reading allows a very efficient reading of streamed file, but it dumps file as single streamed string.
  class(string),    intent(inout)           :: self       !< The string.
  character(len=*), intent(in)              :: file       !< File name.
  logical,          intent(in),    optional :: is_fast    !< Flag to enable (super) fast file reading.
  character(len=*), intent(in),    optional :: form       !< Format of unit.
  integer,          intent(out),   optional :: iostat     !< IO status code.
  character(len=*), intent(inout), optional :: iomsg      !< IO status message.
  logical                                   :: is_fast_   !< Flag to enable (super) fast file reading, local variable.
  type(string)                              :: form_      !< Format of unit, local variable.
  integer                                   :: iostat_    !< IO status code, local variable.
  character(len=:), allocatable             :: iomsg_     !< IO status message, local variable.
  integer                                   :: unit       !< Logical unit.
  logical                                   :: does_exist !< Check if file exist.
  integer(I4P)                              :: filesize   !< Size of the file for fast reading.

  iomsg_ = repeat(' ', 99) ; if (present(iomsg)) iomsg_ = iomsg
  inquire(file=file, iomsg=iomsg_, iostat=iostat_, exist=does_exist)
  if (does_exist) then
    is_fast_ = .false. ; if (present(is_fast)) is_fast_ = is_fast
    if (is_fast_) then
      open(newunit=unit, file=file, access='STREAM', form='UNFORMATTED', iomsg=iomsg_, iostat=iostat_)
      inquire(file=file, size=filesize)
      if (allocated(self%raw)) deallocate(self%raw)
      allocate(character(len=filesize):: self%raw)
      read(unit=unit, iostat=iostat_, iomsg=iomsg_) self%raw
      close(unit)
    else
      form_ = 'FORMATTED' ; if (present(form)) form_ = form ; form_ = form_%upper()
      select case(form_%chars())
      case('FORMATTED')
        open(newunit=unit, file=file, status='OLD', action='READ', iomsg=iomsg_, iostat=iostat_, err=10)
      case('UNFORMATTED')
        open(newunit=unit, file=file, status='OLD', action='READ', form='UNFORMATTED', access='STREAM', &
             iomsg=iomsg_, iostat=iostat_, err=10)
      endselect
      call self%read_lines(unit=unit, form=form, iomsg=iomsg_, iostat=iostat_)
      10 close(unit)
    endif
  else
    iostat_ = 1
    iomsg_ = 'file not found'
  endif
  if (present(iostat)) iostat = iostat_
  if (present(iomsg)) iomsg = iomsg_
  endsubroutine read_file

  subroutine read_line(self, unit, form, iostat, iomsg)
  !< Read line (record) from a connected unit.
  !<
  !< The line is read as an ascii stream read until the eor is reached.
  !<
  !< @note For unformatted read only `access='stream'` is supported with new_line as line terminator.
  class(string),    intent(inout)           :: self    !< The string.
  integer,          intent(in)              :: unit    !< Logical unit.
  character(len=*), intent(in),    optional :: form    !< Format of unit.
  integer,          intent(out),   optional :: iostat  !< IO status code.
  character(len=*), intent(inout), optional :: iomsg   !< IO status message.
  type(string)                              :: form_   !< Format of unit, local variable.
  integer                                   :: iostat_ !< IO status code, local variable.
  character(len=:),          allocatable    :: iomsg_  !< IO status message, local variable.
  character(kind=CK, len=:), allocatable    :: line    !< Line storage.
  character(kind=CK, len=1)                 :: ch      !< Character storage.

  form_ = 'FORMATTED' ; if (present(form)) form_ = form ; form_ = form_%upper()
  iomsg_ = repeat(' ', 99) ; if (present(iomsg)) iomsg_ = iomsg
  line = ''
  select case(form_%chars())
  case('FORMATTED')
    do
      read(unit, "(A)", advance='no', iostat=iostat_, iomsg=iomsg_, err=10, end=10, eor=10) ch
      line = line//ch
    enddo
  case('UNFORMATTED')
    do
      read(unit, iostat=iostat_, iomsg=iomsg_, err=10, end=10) ch
      if (ch==new_line('a')) then
        iostat_ = iostat_eor
        exit
      endif
      line = line//ch
    enddo
  endselect
  10 if (line/='') self%raw = line
  if (present(iostat)) iostat = iostat_
  if (present(iomsg)) iomsg = iomsg_
  endsubroutine read_line

  subroutine read_lines(self, unit, form, iostat, iomsg)
  !< Read (all) lines (records) from a connected unit as a single ascii stream.
  !<
  !< @note All the lines are stored into the string self as a single ascii stream. Each line (record) is separated by a `new_line`
  !< character. The line is read as an ascii stream read until the eor is reached.
  !<
  !< @note The connected unit is rewinded. At a successful exit current record is at eof, at the beginning otherwise.
  !<
  !< @note For unformatted read only `access='stream'` is supported with new_line as line terminator.
  class(string),    intent(inout)           :: self    !< The string.
  integer,          intent(in)              :: unit    !< Logical unit.
  character(len=*), intent(in),    optional :: form    !< Format of unit.
  integer,          intent(out),   optional :: iostat  !< IO status code.
  character(len=*), intent(inout), optional :: iomsg   !< IO status message.
  integer                                   :: iostat_ !< IO status code, local variable.
  character(len=:), allocatable             :: iomsg_  !< IO status message, local variable.
  type(string)                              :: lines   !< Lines storage.
  type(string)                              :: line    !< Line storage.

  iomsg_ = repeat(' ', 99) ; if (present(iomsg)) iomsg_ = iomsg
  rewind(unit)
  iostat_ = 0
  lines%raw = ''
  do
    line%raw = ''
    call line%read_line(unit=unit, form=form, iostat=iostat_, iomsg=iomsg_)
    if (iostat_/=0.and..not.is_iostat_eor(iostat_)) then
      exit
    elseif (line/='') then
      lines%raw = lines%raw//line%raw//new_line('a')
    endif
  enddo
  if (lines%raw/='') self%raw = lines%raw
  if (present(iostat)) iostat = iostat_
  if (present(iomsg)) iomsg = iomsg_
  return
  endsubroutine read_lines

  elemental function replace(self, old, new, count) result(replaced)
  !< Return a string with all occurrences of substring old replaced by new.
  class(string),             intent(in)           :: self      !< The string.
  character(kind=CK, len=*), intent(in)           :: old       !< Old substring.
  character(kind=CK, len=*), intent(in)           :: new       !< New substring.
  integer,                   intent(in), optional :: count     !< Number of old occurences to be replaced.
  type(string)                                    :: replaced  !< The string with old replaced by new.
  integer                                         :: r         !< Counter.

  if (allocated(self%raw)) then
    replaced = self
    r = 0
    do
      if (index(replaced%raw, old)>0) then
        replaced = replaced%replace_one_occurrence(old=old, new=new)
        r = r + 1
        if (present(count)) then
          if (r>=count) exit
        endif
      else
        exit
      endif
    enddo
  endif
  endfunction replace

  elemental function reverse(self) result(reversed)
  !< Return a reversed string.
  class(string), intent(in) :: self     !< The string.
  type(string)              :: reversed !< The reversed string.
  integer                   :: length   !< Length of the string.
  integer                   :: c        !< Counter.

  if (allocated(self%raw)) then
    reversed = self
    length = len(self%raw)
    do c=1, length
      reversed%raw(c:c) = self%raw(length-c+1:length-c+1)
    enddo
  endif
  endfunction reverse

  function search(self, tag_start, tag_end, in_string, in_character, istart, iend) result(tag)
  !< Search for *tagged* record into string, return the first record found (if any) matching the tags.
  !<
  !< Optionally, returns the indexes of tag start/end, thus this is not an `elemental` function.
  !<
  !< @note The tagged record is searched into self if allocated otherwise into `in_string` if passed or, eventually, into
  !< `in_character` is passed. If tag is not found the return string is not allocated and the start/end indexes (if requested) are
  !< zero.
  class(string),             intent(in)            :: self         !< The string.
  character(kind=CK, len=*), intent(in)            :: tag_start    !< Start tag.
  character(kind=CK, len=*), intent(in)            :: tag_end      !< End tag.
  type(string),              intent(in),  optional :: in_string    !< Search into this string.
  character(kind=CK, len=*), intent(in),  optional :: in_character !< Search into this character string.
  integer,                   intent(out), optional :: istart       !< Starting index of tag inside the string.
  integer,                   intent(out), optional :: iend         !< Ending index of tag inside the string.
  type(string)                                     :: tag          !< First tag found.
  character(kind=CK, len=:), allocatable           :: raw          !< Raw string into which search the tag.
  integer                                          :: istart_      !< Starting index of tag inside the string, local variable.
  integer                                          :: iend_        !< Ending index of tag inside the string, local variable.
  integer                                          :: nested_tags  !< Number of nested tags inside tag.
  integer                                          :: t            !< Counter.

  raw = ''
  if (present(in_string)) then
    raw = in_string%raw
  elseif (present(in_character)) then
    raw = in_character
  else
    if (allocated(self%raw)) then
      raw = self%raw
    endif
  endif
  istart_ = 0
  iend_ = 0
  if (raw/='') then
    istart_ = index(raw, tag_start)
    iend_ = index(raw, tag_end)
    if (istart_>0.and.iend_>0) then
      iend_ = iend_ + len(tag_end) - 1
      tag%raw = raw(istart_:iend_)
      nested_tags = tag%count(tag_start)
      if (nested_tags>1) then
        do t=2, nested_tags
          iend_ = iend_ + len(tag_end) - 1 + index(raw(iend_+1:), tag_end)
        enddo
        tag%raw = raw(istart_:iend_)
      endif
    endif
  endif
  if (present(istart)) istart = istart_
  if (present(iend)) iend = iend_
  endfunction search

  pure function slice(self, istart, iend) result(raw)
  !< Return the raw characters data sliced.
  !<
  !<### Example
  !<
  !<```fortran
  !< type(string) :: astring        !< A string.
  !< astring = 'the Quick Brown fox Jumps over the Lazy Dog.'
  !< print "(A)", astring%slice(11,25) ! print "Brown fox Jumps"
  !<```
  class(string), intent(in)              :: self   !< The string.
  integer,       intent(in)              :: istart !< Slice start index.
  integer,       intent(in)              :: iend   !< Slice end   index.
  character(kind=CK, len=:), allocatable :: raw    !< Raw characters data.

  if (allocated(self%raw)) then
    raw = self%raw(istart:iend)
  else
    raw = ''
  endif
  endfunction slice

  elemental function snakecase(self, sep)
  !< Return a string with all words lowercase separated by "_".
  !<
  !< @note Multiple subsequent separators are collapsed to one occurence.
  class(string),             intent(in)           :: self      !< The string.
  character(kind=CK, len=*), intent(in), optional :: sep       !< Separator.
  type(string)                                    :: snakecase !< Snake case string.
  type(string), allocatable                       :: tokens(:) !< String tokens.

  if (allocated(self%raw)) then
    call self%split(tokens=tokens, sep=sep)
    tokens = tokens%lower()
    snakecase = snakecase%join(array=tokens, sep='_')
  endif
  endfunction snakecase

  pure subroutine split(self, tokens, sep, max_tokens)
  !< Return a list of substring in the string, using sep as the delimiter string.
  !<
  !< @note Multiple subsequent separators are collapsed to one occurrence.
  !<
  !< @note If `max_tokens` is passed the returned number of tokens is either `max_tokens` or `max_tokens + 1`.
  class(string),             intent(in)           :: self           !< The string.
  type(string), allocatable, intent(out)          :: tokens(:)      !< Tokens substring.
  character(kind=CK, len=*), intent(in), optional :: sep            !< Separator.
  integer,                   intent(in), optional :: max_tokens     !< Fix the maximum number of returned tokens.
  character(kind=CK, len=:), allocatable          :: sep_           !< Separator, default value.
  integer                                         :: No             !< Number of occurrences of sep.
  integer                                         :: t              !< Character counter.
  type(string)                                    :: temporary      !< Temporary storage.
  type(string), allocatable                       :: temp_toks(:,:) !< Temporary tokens substring.

  if (allocated(self%raw)) then
    sep_ = SPACE ; if (present(sep)) sep_ = sep

    temporary = self%unique(sep_)
    No = temporary%count(sep_)

    if (No>0) then
      if (present(max_tokens)) then
        if (max_tokens < No.and.max_tokens > 0) No = max_tokens
      endif
      allocate(temp_toks(3, No))
      temp_toks(:, 1) = temporary%partition(sep_)
      if (No>1) then
        do t=2, No
          temp_toks(:, t) = temp_toks(3, t-1)%partition(sep_)
        enddo
      endif

      if (temp_toks(1, 1)%raw/=''.and.temp_toks(3, No)%raw/='') then
        allocate(tokens(No+1))
        do t=1, No
          if (t==No) then
            tokens(t  ) = temp_toks(1, t)
            tokens(t+1) = temp_toks(3, t)
          else
            tokens(t) = temp_toks(1, t)
          endif
        enddo
      elseif (temp_toks(1, 1)%raw/='') then
        allocate(tokens(No))
        do t=1, No
          tokens(t) = temp_toks(1, t)
        enddo
      elseif (temp_toks(3, No)%raw/='') then
        allocate(tokens(No))
        do t=1, No-1
          tokens(t) = temp_toks(1, t+1)
        enddo
        tokens(No) = temp_toks(3, No)
      else
        allocate(tokens(No-1))
        do t=2, No
          tokens(t-1) = temp_toks(1, t)
        enddo
      endif

    else
      allocate(tokens(1))
      tokens(1) = self
    endif
  endif
  endsubroutine split

  pure subroutine split_chunked(self, tokens, chunks, sep)
  !< Return a list of substring in the string, using sep as the delimiter string, chunked (memory-efficient) algorithm.
  !<
  !< @note Multiple subsequent separators are collapsed to one occurrence.
  !<
  !< @note The split is performed in chunks of `#chunks` to avoid excessive memory consumption.
  class(string),             intent(in)           :: self      !< The string.
  type(string), allocatable, intent(out)          :: tokens(:) !< Tokens substring.
  integer,                   intent(in)           :: chunks    !< Number of chunks.
  character(kind=CK, len=*), intent(in), optional :: sep       !< Separator.
  character(kind=CK, len=:), allocatable          :: sep_      !< Separator, default value.
  integer                                         :: Nt        !< Number of actual tokens.
  integer                                         :: t         !< Counter.

  if (allocated(self%raw)) then
    sep_ = SPACE ; if (present(sep)) sep_ = sep

    Nt = self%count(sep_)
    if (self%start_with(prefix=sep_)) Nt = Nt - 1
    if (self%end_with(suffix=sep_)) Nt = Nt - 1
    t = 0
    call self%split(tokens=tokens, sep=sep_, max_tokens=chunks)
    do
      t = size(tokens, dim=1)
      if (t > Nt) exit
      call split_last_token(tokens=tokens, max_tokens=chunks)
    enddo

    t = size(tokens, dim=1)
    if (tokens(t)%count(sep_) > 0) then
       call split_last_token(tokens=tokens)
    endif
  endif

  contains
     pure subroutine split_last_token(tokens, max_tokens)
     !< Split last token.
     type(string), allocatable, intent(inout)        :: tokens(:)      !< Tokens substring.
     integer,                   intent(in), optional :: max_tokens     !< Max tokens returned.
     type(string), allocatable                       :: tokens_(:)     !< Temporary tokens.
     type(string), allocatable                       :: tokens_swap(:) !< Swap tokens.
     integer                                         :: Nt_            !< Number of last created tokens.

     call tokens(t)%split(tokens=tokens_, sep=sep_, max_tokens=max_tokens)
     if (allocated(tokens_)) then
       Nt_ = size(tokens_, dim=1)
       if (Nt_ >= 1) then
         allocate(tokens_swap(1:t-1+Nt_))
         tokens_swap(1:t-1) = tokens(1:t-1)
         tokens_swap(t:)    = tokens_(:)
         call move_alloc(from=tokens_swap, to=tokens)
       endif
       deallocate(tokens_)
     endif
     endsubroutine split_last_token
  endsubroutine split_chunked

  elemental function startcase(self, sep)
  !< Return a string with all words capitalized, e.g. title case.
  !<
  !< @note Multiple subsequent separators are collapsed to one occurence.
  class(string),             intent(in)           :: self      !< The string.
  character(kind=CK, len=*), intent(in), optional :: sep       !< Separator.
  type(string)                                    :: startcase !< Start case string.
  character(kind=CK, len=:), allocatable          :: sep_      !< Separator, default value.
  type(string), allocatable                       :: tokens(:) !< String tokens.

  if (allocated(self%raw)) then
    sep_ = SPACE ; if (present(sep)) sep_ = sep
    call self%split(tokens=tokens, sep=sep_)
    tokens = tokens%capitalize()
    startcase = startcase%join(array=tokens, sep=sep_)
  endif
  endfunction startcase

  elemental function strip(self, remove_nulls)
  !< Return a copy of the string with the leading and trailing characters removed.
  class(string), intent(in)           :: self         !< The string.
  logical,       intent(in), optional :: remove_nulls !< Remove null characters at the end.
  type(string)                        :: strip        !< The stripped string.
  integer                             :: c            !< Counter.

  if (allocated(self%raw)) then
    strip = self%adjustl()
    strip = strip%trim()
    if (present(remove_nulls)) then
      if (remove_nulls) then
        c = index(self%raw, char(0))
        if (c>0) strip%raw = strip%raw(1:c-1)
      endif
    endif
  endif
  endfunction strip

  elemental function swapcase(self)
  !< Return a copy of the string with uppercase characters converted to lowercase and vice versa.
  class(string), intent(in) :: self     !< The string.
  type(string)              :: swapcase !< Upper case string.
  integer                   :: n1       !< Characters counter.
  integer                   :: n2       !< Characters counter.

  if (allocated(self%raw)) then
    swapcase = self
    do n1=1, len(self%raw)
      n2 = index(UPPER_ALPHABET, self%raw(n1:n1))
      if (n2>0) then
        swapcase%raw(n1:n1) = LOWER_ALPHABET(n2:n2)
      else
        n2 = index(LOWER_ALPHABET, self%raw(n1:n1))
        if (n2>0) swapcase%raw(n1:n1) = UPPER_ALPHABET(n2:n2)
      endif
    enddo
  endif
  endfunction swapcase

  function tempname(self, is_file, prefix, path)
  !< Return a safe temporary name suitable for temporary file or directories.
  class(string), intent(in)           :: self                   !< The string.
  logical,       intent(in), optional :: is_file                !< True if tempname should be used for file (the default).
  character(*),  intent(in), optional :: prefix                 !< Name prefix, otherwise self is used (if allocated).
  character(*),  intent(in), optional :: path                   !< Path where file/directory should be used, default `./`.
  character(len=:), allocatable       :: tempname               !< Safe (unique) temporary name.
  logical                             :: is_file_               !< True if tempname should be used for file (the default).
  character(len=:), allocatable       :: prefix_                !< Name prefix, otherwise self is used (if allocated).
  character(len=:), allocatable       :: path_                  !< Path where file/directory should be used, default `./`.
  logical, save                       :: is_initialized=.false. !< Status of random seed initialization.
  real(R4P)                           :: random_real            !< Random number (real).
  integer(I4P)                        :: random_integer         !< Random number (integer).
  logical                             :: is_hold                !< Flag to check if a safe tempname has been found.

  is_file_ = .true. ; if (present(is_file)) is_file_ = is_file
  path_ = '' ; if (present(path)) path_ = path
  prefix_ = ''
  if (present(prefix)) then
    prefix_ = prefix
  elseif (allocated(self%raw)) then
    prefix_ = self%raw
  endif
  if (.not.is_initialized) then
    call random_seed
    is_initialized = .true.
  endif
  tempname = repeat(' ', len(path_) + len(prefix_) + 10) ! [path_] + [prefix_] + 6 random chars + [.tmp]
  do
    call random_number(random_real)
    random_integer = transfer(random_real, random_integer)
    random_integer = iand(random_integer, 16777215_I4P)
    if (is_file_)  then
      write(tempname, '(A,Z6.6,A)') path_//prefix_, random_integer, '.tmp'
    else
      write(tempname, '(A,Z6.6)') path_//prefix_, random_integer
      tempname = trim(tempname)
    endif
    inquire(file=tempname, exist=is_hold)
    if (.not.is_hold) exit
  enddo
  endfunction tempname

  elemental function to_integer_I1P(self, kind) result(to_number)
  !< Cast string to integer (I1P).
  class(string), intent(in) :: self      !< The string.
  integer(I1P),  intent(in) :: kind      !< Mold parameter for kind detection.
  integer(I1P)              :: to_number !< The number into the string.

  if (allocated(self%raw)) then
    if (self%is_integer()) read(self%raw, *) to_number
  endif
  endfunction to_integer_I1P

  elemental function to_integer_I2P(self, kind) result(to_number)
  !< Cast string to integer (I2P).
  class(string), intent(in) :: self      !< The string.
  integer(I2P),  intent(in) :: kind      !< Mold parameter for kind detection.
  integer(I2P)              :: to_number !< The number into the string.

  if (allocated(self%raw)) then
    if (self%is_integer()) read(self%raw, *) to_number
  endif
  endfunction to_integer_I2P

  elemental function to_integer_I4P(self, kind) result(to_number)
  !< Cast string to integer (I4P).
  class(string), intent(in) :: self      !< The string.
  integer(I4P),  intent(in) :: kind      !< Mold parameter for kind detection.
  integer(I4P)              :: to_number !< The number into the string.

  if (allocated(self%raw)) then
    if (self%is_integer()) read(self%raw, *) to_number
  endif
  endfunction to_integer_I4P

  elemental function to_integer_I8P(self, kind) result(to_number)
  !< Cast string to integer (I8P).
  class(string), intent(in) :: self      !< The string.
  integer(I8P),  intent(in) :: kind      !< Mold parameter for kind detection.
  integer(I8P)              :: to_number !< The number into the string.

  if (allocated(self%raw)) then
    if (self%is_integer()) read(self%raw, *) to_number
  endif
  endfunction to_integer_I8P

  elemental function to_real_R4P(self, kind) result(to_number)
  !< Cast string to real (R4P).
  class(string), intent(in) :: self      !< The string.
  real(R4P),     intent(in) :: kind      !< Mold parameter for kind detection.
  real(R4P)                 :: to_number !< The number into the string.

  if (allocated(self%raw)) then
    if (self%is_real()) read(self%raw, *) to_number
  endif
  endfunction to_real_R4P

  elemental function to_real_R8P(self, kind) result(to_number)
  !< Cast string to real (R8P).
  class(string), intent(in) :: self      !< The string.
  real(R8P),     intent(in) :: kind      !< Mold parameter for kind detection.
  real(R8P)                 :: to_number !< The number into the string.

  if (allocated(self%raw)) then
    if (self%is_real()) read(self%raw, *) to_number
  endif
  endfunction to_real_R8P

  elemental function to_real_R16P(self, kind) result(to_number)
  !< Cast string to real (R16P).
  class(string), intent(in) :: self      !< The string.
  real(R16P),    intent(in) :: kind      !< Mold parameter for kind detection.
  real(R16P)                :: to_number !< The number into the string.

  if (allocated(self%raw)) then
    if (self%is_real()) read(self%raw, *) to_number
  endif
  endfunction to_real_R16P

  elemental function unescape(self, to_unescape, unesc) result(unescaped)
  !< Unescape double backslashes (or custom escaped character).
  class(string),             intent(in)           :: self        !< The string.
  character(kind=CK, len=1), intent(in)           :: to_unescape !< Character to be unescaped.
  character(kind=CK, len=*), intent(in), optional :: unesc       !< Character used to unescape.
  type(string)                                    :: unescaped   !< Escaped string.
  character(kind=CK, len=:), allocatable          :: unesc_      !< Character to unescape, local variable.
  integer                                         :: c           !< Character counter.

  if (allocated(self%raw)) then
    unesc_ = '' ; if (present(unesc)) unesc_ = unesc
    unescaped%raw = ''
    c = 1
    do
      if (c>len(self%raw)) exit
      if (c==len(self%raw)) then
        unescaped%raw = unescaped%raw//self%raw(c:c)
        exit
      else
        if (self%raw(c:c+1)==BACKSLASH//to_unescape) then
          unescaped%raw = unescaped%raw//to_unescape
          c = c + 2
        else
          unescaped%raw = unescaped%raw//self%raw(c:c)
          c = c + 1
        endif
      endif
    enddo
  endif
  endfunction unescape

  elemental function unique(self, substring) result(uniq)
  !< Reduce to one (unique) multiple (sequential) occurrences of a substring into a string.
  !<
  !< For example the string ' ab-cre-cre-ab' is reduce to 'ab-cre-ab' if the substring is '-cre'.
  !< @note Eventual multiple trailing white space are not reduced to one occurrence.
  class(string),             intent(in)           :: self       !< The string.
  character(kind=CK, len=*), intent(in), optional :: substring  !< Substring which multiple occurences must be reduced to one.
  character(kind=CK, len=:), allocatable          :: substring_ !< Substring, default value.
  type(string)                                    :: uniq       !< String parsed.

  if (allocated(self%raw)) then
    substring_ = SPACE ; if (present(substring)) substring_ = substring

    uniq = self
    do
      if (.not.uniq%index(repeat(substring_, 2))>0) exit
      uniq = uniq%replace(old=repeat(substring_, 2), new=substring_)
    enddo
  endif
  endfunction unique

  elemental function upper(self)
  !< Return a string with all uppercase characters.
  class(string), intent(in) :: self  !< The string.
  type(string)              :: upper !< Upper case string.
  integer                   :: n1    !< Characters counter.
  integer                   :: n2    !< Characters counter.

  if (allocated(self%raw)) then
    upper = self
    do n1=1, len(self%raw)
      n2 = index(LOWER_ALPHABET, self%raw(n1:n1))
      if (n2>0) upper%raw(n1:n1) = UPPER_ALPHABET(n2:n2)
    enddo
  endif
  endfunction upper

  subroutine write_file(self, file, form, iostat, iomsg)
  !< Write a single string stream into file.
  !<
  !< @note For unformatted read only `access='stream'` is supported with new_line as line terminator.
  class(string),    intent(in)              :: self    !< The string.
  character(len=*), intent(in)              :: file    !< File name.
  character(len=*), intent(in),    optional :: form    !< Format of unit.
  integer,          intent(out),   optional :: iostat  !< IO status code.
  character(len=*), intent(inout), optional :: iomsg   !< IO status message.
  type(string)                              :: form_   !< Format of unit, local variable.
  integer                                   :: iostat_ !< IO status code, local variable.
  character(len=:), allocatable             :: iomsg_  !< IO status message, local variable.
  integer                                   :: unit    !< Logical unit.

  iomsg_ = repeat(' ', 99) ; if (present(iomsg)) iomsg_ = iomsg
  form_ = 'FORMATTED' ; if (present(form)) form_ = form ; form_ = form_%upper()
  select case(form_%chars())
  case('FORMATTED')
    open(newunit=unit, file=file, action='WRITE', iomsg=iomsg_, iostat=iostat_, err=10)
  case('UNFORMATTED')
    open(newunit=unit, file=file, action='WRITE', form='UNFORMATTED', access='STREAM', iomsg=iomsg_, iostat=iostat_, err=10)
  endselect
  call self%write_lines(unit=unit, form=form, iomsg=iomsg_, iostat=iostat_)
  10 close(unit)
  if (present(iostat)) iostat = iostat_
  if (present(iomsg)) iomsg = iomsg_
  endsubroutine write_file

  subroutine write_line(self, unit, form, iostat, iomsg)
  !< Write line (record) to a connected unit.
  !<
  !< @note If the connected unit is unformatted a `new_line()` character is added at the end (if necessary) to mark the end of line.
  class(string),    intent(in)              :: self    !< The string.
  integer,          intent(in)              :: unit    !< Logical unit.
  character(len=*), intent(in),    optional :: form    !< Format of unit.
  integer,          intent(out),   optional :: iostat  !< IO status code.
  character(len=*), intent(inout), optional :: iomsg   !< IO status message.
  type(string)                              :: form_   !< Format of unit, local variable.
  integer                                   :: iostat_ !< IO status code, local variable.
  character(len=:), allocatable             :: iomsg_  !< IO status message, local variable.

  iostat_ = 0
  iomsg_ = repeat(' ', 99) ; if (present(iomsg)) iomsg_ = iomsg
  if (allocated(self%raw)) then
    form_ = 'FORMATTED' ; if (present(form)) form_ = form ; form_ = form_%upper()
    select case(form_%chars())
    case('FORMATTED')
      write(unit, "(A)", iostat=iostat_, iomsg=iomsg_) self%raw
    case('UNFORMATTED')
      if (self%end_with(new_line('a'))) then
        write(unit, iostat=iostat_, iomsg=iomsg_) self%raw
      else
        write(unit, iostat=iostat_, iomsg=iomsg_) self%raw//new_line('a')
      endif
    endselect
  endif
  if (present(iostat)) iostat = iostat_
  if (present(iomsg)) iomsg = iomsg_
  endsubroutine write_line

  subroutine write_lines(self, unit, form, iostat, iomsg)
  !< Write lines (records) to a connected unit.
  !<
  !< This method checks if self contains more than one line (records) and writes them as lines (records).
  !<
  !< @note If the connected unit is unformatted a `new_line()` character is added at the end (if necessary) to mark the end of line.
  class(string),    intent(in)              :: self     !< The string.
  integer,          intent(in)              :: unit     !< Logical unit.
  character(len=*), intent(in),    optional :: form     !< Format of unit.
  integer,          intent(out),   optional :: iostat   !< IO status code.
  character(len=*), intent(inout), optional :: iomsg    !< IO status message.
  type(string), allocatable                 :: lines(:) !< Lines.
  integer                                   :: l        !< Counter.

  if (allocated(self%raw)) then
    call self%split(tokens=lines, sep=new_line('a'))
    do l=1, size(lines, dim=1)
       call lines(l)%write_line(unit=unit, form=form, iostat=iostat, iomsg=iomsg)
    enddo
  endif
  endsubroutine write_lines

  ! inquire
  elemental function end_with(self, suffix, start, end, ignore_null_eof)
  !< Return true if a string ends with a specified suffix.
  class(string),             intent(in)           :: self             !< The string.
  character(kind=CK, len=*), intent(in)           :: suffix           !< Searched suffix.
  integer,                   intent(in), optional :: start            !< Start position into the string.
  integer,                   intent(in), optional :: end              !< End position into the string.
  logical,                   intent(in), optional :: ignore_null_eof  !< Ignore null character at the end of file.
  logical                                         :: end_with         !< Result of the test.
  integer                                         :: start_           !< Start position into the string, local variable.
  integer                                         :: end_             !< End position into the string, local variable.
  logical                                         :: ignore_null_eof_ !< Ignore null character at the end of file, local variable.

  end_with = .false.
  if (allocated(self%raw)) then
    start_           = 1             ; if (present(start))           start_           = start
    end_             = len(self%raw) ; if (present(end))             end_             = end
    ignore_null_eof_ = .false.       ; if (present(ignore_null_eof)) ignore_null_eof_ = ignore_null_eof
    if (ignore_null_eof_.and.(self%raw(end_:end_) == char(0))) end_ = end_ - 1
    if (len(suffix) <= len(self%raw(start_:end_))) then
      end_with = self%raw(end_-len(suffix)+1:end_) == suffix
    endif
  endif
  endfunction end_with

  elemental function is_allocated(self)
  !< Return true if the string is allocated.
  class(string), intent(in) :: self     !< The string.
  logical                   :: is_allocated !< Result of the test.

  is_allocated = allocated(self%raw)
  endfunction is_allocated

  elemental function is_digit(self)
  !< Return true if all characters in the string are digits.
  class(string), intent(in) :: self     !< The string.
  logical                   :: is_digit !< Result of the test.
  integer                   :: c        !< Character counter.

  is_digit = .false.
  if (allocated(self%raw)) then
    do c=1, len(self%raw)
      select case (self%raw(c:c))
      case ('0':'9')
        is_digit = .true.
      case default
        is_digit = .false.
        exit
      end select
    enddo
  endif
  endfunction is_digit

  elemental function is_integer(self, allow_spaces)
  !< Return true if the string contains an integer.
  !<
  !< The regular expression is `\s*[\+\-]?\d+([eE]\+?\d+)?\s*`. The parse algorithm is done in stages:
  !<
  !< | S0  | S1      | S2  | S3   | S4  | S5  | S6  |
  !< |-----|---------|-----|------|-----|-----|-----|
  !< |`\s*`|`[\+\-]?`|`\d+`|`[eE]`|`\+?`|`\d+`|`\s*`|
  !<
  !< Exit on stages-parsing results in:
  !<
  !< | S0 | S1 | S2 | S3 | S4 | S5 | S6 |
  !< |----|----|----|----|----|----|----|
  !< |  F |  F |  T |  F |  F |  T |  T |
  !<
  !< @note This implementation is courtesy of
  !< [tomedunn](https://github.com/tomedunn/fortran-string-utility-module/blob/master/src/string_utility_module.f90#L294)
  class(string), intent(in)           :: self          !< The string.
  logical,       intent(in), optional :: allow_spaces  !< Allow leading-trailing spaces.
  logical                             :: is_integer    !< Result of the test.
  logical                             :: allow_spaces_ !< Allow leading-trailing spaces, local variable.
  integer                             :: stage         !< Stages counter.
  integer                             :: c             !< Character counter.

  if (allocated(self%raw)) then
    allow_spaces_ = .true. ; if (present(allow_spaces)) allow_spaces_ = allow_spaces
    stage = 0
    is_integer = .true.
    do c=1, len(self%raw)
      select case(self%raw(c:c))
      case(SPACE, TAB)
        select case(stage)
        case(0, 6)
          is_integer = allow_spaces_
        case(2, 5)
          is_integer = allow_spaces_
          stage = 6
        case default
          is_integer = .false.
        endselect
      case('-')
        select case(stage)
        case(0)
          stage = 1
        case default
          is_integer = .false.
        end select
      case('+')
        select case(stage)
        case(0)
          stage = 1
        case(3)
          stage = 4
        case default
          is_integer = .false.
        endselect
      case('0':'9')
        select case(stage)
        case(0:1)
          stage = 2
        case(3:4)
          stage = 5
        case default
          continue
        endselect
      case ('e','E')
        select case(stage)
        case(2)
          stage = 3
        case default
          is_integer = .false.
        endselect
      case default
        is_integer = .false.
      endselect
      if (.not.is_integer) exit
    enddo
  endif
  if (is_integer) then
    select case(stage)
    case(2, 5, 6)
      is_integer = .true.
    case default
      is_integer = .false.
    end select
  endif
  endfunction is_integer

  elemental function is_lower(self)
  !< Return true if all characters in the string are lowercase.
  class(string), intent(in) :: self     !< The string.
  logical                   :: is_lower !< Result of the test.
  integer                   :: c        !< Character counter.

  is_lower = .false.
  if (allocated(self%raw)) then
    is_lower = .true.
    do c=1, len(self%raw)
      if (index(UPPER_ALPHABET, self%raw(c:c))>0) then
        is_lower = .false.
        exit
      endif
    enddo
  endif
  endfunction is_lower

  elemental function is_number(self, allow_spaces)
  !< Return true if the string contains a number (real or integer).
  class(string), intent(in)           :: self         !< The string.
  logical,       intent(in), optional :: allow_spaces !< Allow leading-trailing spaces.
  logical                             :: is_number    !< Result of the test.

  is_number = (self%is_integer(allow_spaces=allow_spaces).or.self%is_real(allow_spaces=allow_spaces))
  endfunction is_number

  elemental function is_real(self, allow_spaces)
  !< Return true if the string contains a real.
  !<
  !< The regular expression is `\s*[\+\-]?\d*(|\.?\d*([deDE][\+\-]?\d+)?)\s*`. The parse algorithm is done in stages:
  !<
  !< | S0  | S1      | S2  | S3  | S4  | S5     | S6      | S7  | S8  |
  !< |-----|---------|-----|-----|-----|--------|---------|-----|-----|
  !< |`\s*`|`[\+\-]?`|`\d*`|`\.?`|`\d*`|`[deDE]`|`[\+\-]?`|`\d*`|`\s*`|
  !<
  !< Exit on stages-parsing results in:
  !<
  !< | S0 | S1 | S2 | S3 | S4 | S5 | S6 | S7 | S8 |
  !< |----|----|----|----|----|----|----|----|----|
  !  |  F |  F |  T |  T |  T |  F |  F |  T |  T |
  !<
  !< @note This implementation is courtesy of
  !< [tomedunn](https://github.com/tomedunn/fortran-string-utility-module/blob/master/src/string_utility_module.f90#L614)
  class(string), intent(in)           :: self              !< The string.
  logical,       intent(in), optional :: allow_spaces      !< Allow leading-trailing spaces.
  logical                             :: is_real           !< Result of the test.
  logical                             :: allow_spaces_     !< Allow leading-trailing spaces, local variable.
  logical                             :: has_leading_digit !< Check the presence of leading digits.
  integer                             :: stage             !< Stages counter.
  integer                             :: c                 !< Character counter.

  if (allocated(self%raw)) then
    allow_spaces_ = .true. ; if (present(allow_spaces)) allow_spaces_ = allow_spaces
    stage = 0
    is_real = .true.
    has_leading_digit = .false.
    do c=1, len(self%raw)
      select case(self%raw(c:c))
      case(SPACE, TAB)
        select case(stage)
        case(0, 8)
          is_real = allow_spaces_
          continue
        case(2:4, 7)
          is_real = allow_spaces_
          stage = 8
        case default
          is_real = .false.
        endselect
      case('+', '-')
        select case(stage)
        case(0)
          stage = 1
        case(5)
          stage = 6
        case default
          is_real = .false.
        endselect
      case('0':'9')
        select case(stage)
        case(0:1)
          stage = 2
          has_leading_digit = .true.
        case(3)
          stage = 4
        case(5:6)
          stage = 7
        case default
          continue
        endselect
      case('.')
        select case(stage)
        case(0:2)
          stage = 3
        case default
          is_real = .false.
        endselect
      case('e','E','d','D')
        select case(stage)
        case(2:4)
          stage = 5
        case default
          is_real = .false.
        endselect
      case default
        is_real = .false.
      endselect
      if (.not.is_real) exit
    enddo
  endif
  if (is_real) then
    select case(stage)
    case(2, 4, 7, 8)
      is_real = .true.
    case(3)
      is_real = has_leading_digit
    case default
      is_real = .false.
    endselect
  endif
  endfunction is_real

  elemental function is_upper(self)
  !< Return true if all characters in the string are uppercase.
  class(string), intent(in) :: self     !< The string.
  logical                   :: is_upper !< Result of the test.
  integer                   :: c        !< Character counter.

  is_upper = .false.
  if (allocated(self%raw)) then
    is_upper = .true.
    do c=1, len(self%raw)
      if (index(LOWER_ALPHABET, self%raw(c:c))>0) then
        is_upper = .false.
        exit
      endif
    enddo
  endif
  endfunction is_upper

  elemental function start_with(self, prefix, start, end)
  !< Return true if a string starts with a specified prefix.
  class(string),             intent(in)           :: self       !< The string.
  character(kind=CK, len=*), intent(in)           :: prefix     !< Searched prefix.
  integer,                   intent(in), optional :: start      !< Start position into the string.
  integer,                   intent(in), optional :: end        !< End position into the string.
  logical                                         :: start_with !< Result of the test.
  integer                                         :: start_     !< Start position into the string, local variable.
  integer                                         :: end_       !< End position into the string, local variable.

  start_with = .false.
  if (allocated(self%raw)) then
    start_ = 1             ; if (present(start)) start_ = start
    end_   = len(self%raw) ; if (present(end))   end_   = end
    if (len(prefix)<=len(self%raw(start_:end_))) then
      start_with = index(self%raw(start_:end_), prefix)==1
    endif
  endif
  endfunction start_with

  ! private methods

  ! assignments
  elemental subroutine string_assign_string(lhs, rhs)
  !< Assignment operator from string input.
  class(string), intent(inout) :: lhs !< Left hand side.
  type(string),  intent(in)    :: rhs !< Right hand side.

  if (allocated(rhs%raw)) lhs%raw = rhs%raw
  endsubroutine string_assign_string

  elemental subroutine string_assign_character(lhs, rhs)
  !< Assignment operator from character input.
  class(string),             intent(inout) :: lhs !< Left hand side.
  character(kind=CK, len=*), intent(in)    :: rhs !< Right hand side.

  lhs%raw = rhs
  endsubroutine string_assign_character

  elemental subroutine string_assign_integer_I1P(lhs, rhs)
  !< Assignment operator from real input.
  class(string), intent(inout) :: lhs !< Left hand side.
  integer(I1P),  intent(in)    :: rhs !< Right hand side.

  lhs%raw = trim(str(rhs))
  endsubroutine string_assign_integer_I1P

  elemental subroutine string_assign_integer_I2P(lhs, rhs)
  !< Assignment operator from real input.
  class(string), intent(inout) :: lhs !< Left hand side.
  integer(I2P),  intent(in)    :: rhs !< Right hand side.

  lhs%raw = trim(str(rhs))
  endsubroutine string_assign_integer_I2P

  elemental subroutine string_assign_integer_I4P(lhs, rhs)
  !< Assignment operator from real input.
  class(string), intent(inout) :: lhs !< Left hand side.
  integer(I4P),  intent(in)    :: rhs !< Right hand side.

  lhs%raw = trim(str(rhs))
  endsubroutine string_assign_integer_I4P

  elemental subroutine string_assign_integer_I8P(lhs, rhs)
  !< Assignment operator from real input.
  class(string), intent(inout) :: lhs !< Left hand side.
  integer(I8P),  intent(in)    :: rhs !< Right hand side.

  lhs%raw = trim(str(rhs))
  endsubroutine string_assign_integer_I8P

  elemental subroutine string_assign_real_R4P(lhs, rhs)
  !< Assignment operator from real input.
  class(string), intent(inout) :: lhs !< Left hand side.
  real(R4P),     intent(in)    :: rhs !< Right hand side.

  lhs%raw = trim(str(rhs))
  endsubroutine string_assign_real_R4P

  elemental subroutine string_assign_real_R8P(lhs, rhs)
  !< Assignment operator from real input.
  class(string), intent(inout) :: lhs !< Left hand side.
  real(R8P),     intent(in)    :: rhs !< Right hand side.

  lhs%raw = trim(str(rhs))
  endsubroutine string_assign_real_R8P

  elemental subroutine string_assign_real_R16P(lhs, rhs)
  !< Assignment operator from real input.
  class(string), intent(inout) :: lhs !< Left hand side.
  real(R16P),    intent(in)    :: rhs !< Right hand side.

  lhs%raw = trim(str(rhs))
  endsubroutine string_assign_real_R16P

  ! contatenation operators
  pure function string_concat_string(lhs, rhs) result(concat)
  !< Concatenation with string.
  class(string), intent(in)              :: lhs    !< Left hand side.
  type(string),  intent(in)              :: rhs    !< Right hand side.
  character(kind=CK, len=:), allocatable :: concat !< Concatenated string.

  concat = ''
  if (allocated(lhs%raw)) concat = lhs%raw
  if (allocated(rhs%raw)) concat = concat//rhs%raw
  endfunction string_concat_string

  pure function string_concat_character(lhs, rhs) result(concat)
  !< Concatenation with character.
  class(string),             intent(in)  :: lhs    !< Left hand side.
  character(kind=CK, len=*), intent(in)  :: rhs    !< Right hand side.
  character(kind=CK, len=:), allocatable :: concat !< Concatenated string.

  if (allocated(lhs%raw)) then
    concat = lhs%raw//rhs
  else
    concat = rhs
  endif
  endfunction string_concat_character

  pure function character_concat_string(lhs, rhs) result(concat)
  !< Concatenation with character (inverted).
  character(kind=CK, len=*), intent(in)  :: lhs    !< Left hand side.
  class(string),             intent(in)  :: rhs    !< Right hand side.
  character(kind=CK, len=:), allocatable :: concat !< Concatenated string.

  if (allocated(rhs%raw)) then
    concat = lhs//rhs%raw
  else
    concat = lhs
  endif
  endfunction character_concat_string

  elemental function string_concat_string_string(lhs, rhs) result(concat)
  !< Concatenation with string.
  class(string), intent(in)              :: lhs       !< Left hand side.
  type(string),  intent(in)              :: rhs       !< Right hand side.
  type(string)                           :: concat    !< Concatenated string.
  character(kind=CK, len=:), allocatable :: temporary !< Temporary concatenated string.

  temporary = ''
  if (allocated(lhs%raw)) temporary = lhs%raw
  if (allocated(rhs%raw)) temporary = temporary//rhs%raw
  if (temporary/='') concat%raw = temporary
  endfunction string_concat_string_string

  elemental function string_concat_character_string(lhs, rhs) result(concat)
  !< Concatenation with character.
  class(string),             intent(in)  :: lhs    !< Left hand side.
  character(kind=CK, len=*), intent(in)  :: rhs    !< Right hand side.
  type(string)                           :: concat !< Concatenated string.

  if (allocated(lhs%raw)) then
    concat%raw = lhs%raw//rhs
  else
    concat%raw = rhs
  endif
  endfunction string_concat_character_string

  elemental function character_concat_string_string(lhs, rhs) result(concat)
  !< Concatenation with character (inverted).
  character(kind=CK, len=*), intent(in)  :: lhs    !< Left hand side.
  class(string),             intent(in)  :: rhs    !< Right hand side.
  type(string)                           :: concat !< Concatenated string.

  if (allocated(rhs%raw)) then
    concat%raw = lhs//rhs%raw
  else
    concat%raw = lhs
  endif
  endfunction character_concat_string_string

  ! logical operators
  elemental function string_eq_string(lhs, rhs) result(is_it)
  !< Equal to string logical operator.
  class(string), intent(in) :: lhs   !< Left hand side.
  type(string),  intent(in) :: rhs   !< Right hand side.
  logical                   :: is_it !< Opreator test result.

  is_it = lhs%raw == rhs%raw
  endfunction string_eq_string

  elemental function string_eq_character(lhs, rhs) result(is_it)
  !< Equal to character logical operator.
  class(string),             intent(in) :: lhs   !< Left hand side.
  character(kind=CK, len=*), intent(in) :: rhs   !< Right hand side.
  logical                               :: is_it !< Opreator test result.

  is_it = lhs%raw == rhs
  endfunction string_eq_character

  elemental function character_eq_string(lhs, rhs) result(is_it)
  !< Equal to character (inverted) logical operator.
  character(kind=CK, len=*), intent(in) :: lhs   !< Left hand side.
  class(string),             intent(in) :: rhs   !< Right hand side.
  logical                               :: is_it !< Opreator test result.

  is_it = rhs%raw == lhs
  endfunction character_eq_string

  elemental function string_ne_string(lhs, rhs) result(is_it)
  !< Not equal to string logical operator.
  class(string), intent(in) :: lhs   !< Left hand side.
  type(string),  intent(in) :: rhs   !< Right hand side.
  logical                   :: is_it !< Opreator test result.

  is_it = lhs%raw /= rhs%raw
  endfunction string_ne_string

  elemental function string_ne_character(lhs, rhs) result(is_it)
  !< Not equal to character logical operator.
  class(string),             intent(in) :: lhs   !< Left hand side.
  character(kind=CK, len=*), intent(in) :: rhs   !< Right hand side.
  logical                               :: is_it !< Opreator test result.

  is_it = lhs%raw /= rhs
  endfunction string_ne_character

  elemental function character_ne_string(lhs, rhs) result(is_it)
  !< Not equal to character (inverted) logical operator.
  character(kind=CK, len=*), intent(in) :: lhs   !< Left hand side.
  class(string),             intent(in) :: rhs   !< Right hand side.
  logical                               :: is_it !< Opreator test result.

  is_it = rhs%raw /= lhs
  endfunction character_ne_string

  elemental function string_lt_string(lhs, rhs) result(is_it)
  !< Lower than to string logical operator.
  class(string), intent(in) :: lhs   !< Left hand side.
  type(string),  intent(in) :: rhs   !< Right hand side.
  logical                   :: is_it !< Opreator test result.

  is_it = lhs%raw < rhs%raw
  endfunction string_lt_string

  elemental function string_lt_character(lhs, rhs) result(is_it)
  !< Lower than to character logical operator.
  class(string),             intent(in) :: lhs   !< Left hand side.
  character(kind=CK, len=*), intent(in) :: rhs   !< Right hand side.
  logical                               :: is_it !< Opreator test result.

  is_it = lhs%raw < rhs
  endfunction string_lt_character

  elemental function character_lt_string(lhs, rhs) result(is_it)
  !< Lower than to character (inverted) logical operator.
  character(kind=CK, len=*), intent(in) :: lhs   !< Left hand side.
  class(string),             intent(in) :: rhs   !< Right hand side.
  logical                               :: is_it !< Opreator test result.

  is_it = lhs < rhs%raw
  endfunction character_lt_string

  elemental function string_le_string(lhs, rhs) result(is_it)
  !< Lower equal than to string logical operator.
  class(string), intent(in) :: lhs   !< Left hand side.
  type(string),  intent(in) :: rhs   !< Right hand side.
  logical                   :: is_it !< Opreator test result.

  is_it = lhs%raw <= rhs%raw
  endfunction string_le_string

  elemental function string_le_character(lhs, rhs) result(is_it)
  !< Lower equal than to character logical operator.
  class(string),             intent(in) :: lhs   !< Left hand side.
  character(kind=CK, len=*), intent(in) :: rhs   !< Right hand side.
  logical                               :: is_it !< Opreator test result.

  is_it = lhs%raw <= rhs
  endfunction string_le_character

  elemental function character_le_string(lhs, rhs) result(is_it)
  !< Lower equal than to character (inverted) logical operator.
  character(kind=CK, len=*), intent(in) :: lhs   !< Left hand side.
  class(string),             intent(in) :: rhs   !< Right hand side.
  logical                               :: is_it !< Opreator test result.

  is_it = lhs <= rhs%raw
  endfunction character_le_string

  elemental function string_ge_string(lhs, rhs) result(is_it)
  !< Greater equal than to string logical operator.
  class(string), intent(in) :: lhs   !< Left hand side.
  type(string),  intent(in) :: rhs   !< Right hand side.
  logical                   :: is_it !< Opreator test result.

  is_it = lhs%raw >= rhs%raw
  endfunction string_ge_string

  elemental function string_ge_character(lhs, rhs) result(is_it)
  !< Greater equal than to character logical operator.
  class(string),             intent(in) :: lhs   !< Left hand side.
  character(kind=CK, len=*), intent(in) :: rhs   !< Right hand side.
  logical                               :: is_it !< Opreator test result.

  is_it = lhs%raw >= rhs
  endfunction string_ge_character

  elemental function character_ge_string(lhs, rhs) result(is_it)
  !< Greater equal than to character (inverted) logical operator.
  character(kind=CK, len=*), intent(in) :: lhs   !< Left hand side.
  class(string),             intent(in) :: rhs   !< Right hand side.
  logical                               :: is_it !< Opreator test result.

  is_it = lhs >= rhs%raw
  endfunction character_ge_string

  elemental function string_gt_string(lhs, rhs) result(is_it)
  !< Greater than to string logical operator.
  class(string), intent(in) :: lhs   !< Left hand side.
  type(string),  intent(in) :: rhs   !< Right hand side.
  logical                   :: is_it !< Opreator test result.

  is_it = lhs%raw > rhs%raw
  endfunction string_gt_string

  elemental function string_gt_character(lhs, rhs) result(is_it)
  !< Greater than to character logical operator.
  class(string),             intent(in) :: lhs   !< Left hand side.
  character(kind=CK, len=*), intent(in) :: rhs   !< Right hand side.
  logical                               :: is_it !< Opreator test result.

  is_it = lhs%raw > rhs
  endfunction string_gt_character

  elemental function character_gt_string(lhs, rhs) result(is_it)
  !< Greater than to character (inverted) logical operator.
  character(kind=CK, len=*), intent(in) :: lhs   !< Left hand side.
  class(string),             intent(in) :: rhs   !< Right hand side.
  logical                               :: is_it !< Opreator test result.

  is_it = lhs > rhs%raw
  endfunction character_gt_string

  ! IO
  subroutine read_formatted(dtv, unit, iotype, v_list, iostat, iomsg)
  !< Formatted input.
  !<
  !< @bug Change temporary acks: find a more precise length of the input string and avoid the trimming!
  class(string),             intent(inout) :: dtv         !< The string.
  integer,                   intent(in)    :: unit        !< Logical unit.
  character(len=*),          intent(in)    :: iotype      !< Edit descriptor.
  integer,                   intent(in)    :: v_list(:)   !< Edit descriptor list.
  integer,                   intent(out)   :: iostat      !< IO status code.
  character(len=*),          intent(inout) :: iomsg       !< IO status message.
  character(len=len(iomsg))                :: local_iomsg !< Local variant of iomsg, so it doesn't get inappropriately redefined.
  character(kind=CK, len=1)                :: delim       !< String delimiter, if any.
  character(kind=CK, len=100)              :: temporary   !< Temporary storage string.

  if (iotype == 'LISTDIRECTED') then
    call get_next_non_blank_character_any_record(unit=unit, ch=delim, iostat=iostat, iomsg=iomsg)
    if (iostat/=0) return
    if (delim=='"'.OR.delim=="'") then
      call dtv%read_delimited(unit=unit, delim=delim, iostat=iostat, iomsg=local_iomsg)
    else
      ! step back before the non-blank
      read(unit, "(TL1)", iostat=iostat, iomsg=iomsg)
      if (iostat /= 0) return
      call dtv%read_undelimited_listdirected(unit=unit, iostat=iostat, iomsg=local_iomsg)
    endif
    if (is_iostat_eor(iostat)) then
      ! suppress IOSTAT_EOR
      iostat = 0
    elseif (iostat /= 0) then
      iomsg = local_iomsg
    endif
    return
  else
    read(unit, "(A)", iostat=iostat, iomsg=iomsg)temporary
    dtv%raw = trim(temporary)
  endif
  endsubroutine read_formatted

  subroutine read_delimited(dtv, unit, delim, iostat, iomsg)
  !< Read a delimited string from a unit connected for formatted input.
  !<
  !< If the closing delimiter is followed by end of record, then we return end of record.
  class(string),             intent(out)   :: dtv       !< The string.
  integer,                   intent(in)    :: unit      !< Logical unit.
  character(kind=CK, len=1), intent(in)    :: delim     !< String delimiter.
  integer,                   intent(out)   :: iostat    !< IO status code.
  character(kind=CK, len=*), intent(inout) :: iomsg     !< IO status message.
  character(kind=CK, len=1)                :: ch        !< A character read.
  logical                                  :: was_delim !< Indicates that the last character read was a delimiter.

  was_delim = .false.
  dtv%raw = ''
  do
    read(unit, "(A)", iostat=iostat, iomsg=iomsg) ch
    if (is_iostat_eor(iostat)) then
      if (was_delim) then
        ! end of delimited string followed by end of record is end of the string. Pass back the end of record condition to the
        ! caller
        return
      else
        ! end of record without terminating delimiter - move along
        cycle
      endif
    elseif (iostat /= 0) THEN
      return
    endif
    if (ch == delim) then
      if (was_delim) then
        ! doubled delimiter is one delimiter in the value
        dtv%raw = dtv%raw // ch
        was_delim = .false.
      else
        ! need to test next character to see what is happening
        was_delim = .true.
      endif
    elseif (was_delim) then
      ! the previous character was actually the delimiter for the end of the string. Put back this character
      read(unit, "(TL1)", iostat=iostat, iomsg=iomsg)
      return
    else
      dtv%raw = dtv%raw // ch
    endif
  enddo
  endsubroutine read_delimited

  subroutine read_undelimited_listdirected(dtv, unit, iostat, iomsg)
  !< Read an undelimited (no leading apostrophe or double quote) character value according to the rules for list directed input.
  !<
  !< A blank, comma/semicolon (depending on the decimal mode), slash or end of record terminates the string.
  !<
  !< If input is terminated by end of record, then this procedure returns an end-of-record condition.
  class(string),    intent(inout) :: dtv           !< The string.
  integer,          intent(in)    :: unit          !< Logical unit.
  integer,          intent(out)   :: iostat        !< IO status code.
  character(len=*), intent(inout) :: iomsg         !< IO status message.
  logical                         :: decimal_point !<True if DECIMAL=POINT in effect.

  call get_decimal_mode(unit=unit, decimal_point=decimal_point, iostat=iostat, iomsg=iomsg)
  if (iostat /= 0) return
  call dtv%read_undelimited(unit=unit, terminators=' '//'/'//merge(CK_',', CK_';', decimal_point), iostat=iostat, iomsg=iomsg)
  endsubroutine read_undelimited_listdirected

  subroutine read_undelimited(dtv, unit, terminators, iostat, iomsg)
  !< Read an undelimited string up until end of record or a character from a set of terminators is encountered.
  !<
  !< If a terminator is encountered, the file position will be at that terminating character. If end of record is encountered, the
  !< file remains at end of record.
  class(string),             intent(inout) :: dtv         !< The string.
  integer,                   intent(in)    :: unit        !< Logical unit.
  character(kind=CK, len=*), intent(in)    :: terminators !< Characters that are considered to terminate the string.
                                                          !< Blanks in this string are meaningful.
  integer,                   intent(out)   :: iostat      !< IO status code.
  character(len=*),          intent(inout) :: iomsg       !< IO status message.
  character(kind=CK, len=1)                :: ch          !< A character read.

  dtv%raw = ''
  do
    read(unit, "(A)", iostat=iostat, iomsg=iomsg) ch
    if (is_iostat_eor(iostat)) then
      ! end of record just means end of string. We pass on the condition
      return
    elseif (iostat /= 0) then
      ! something odd happened
      return
    endif
    if (scan(ch, terminators) /= 0) then
      ! change the file position so that the next read sees the terminator
      read(unit, "(TL1)", iostat=iostat, iomsg=iomsg)
      if (iostat /= 0) return
      iostat = 0
      return
    endif
    ! we got a character - append it
    dtv%raw = dtv%raw // ch
  enddo
  endsubroutine read_undelimited

  subroutine write_formatted(dtv, unit, iotype, v_list, iostat, iomsg)
  !< Formatted output.
  class(string),             intent(in)    :: dtv       !< The string.
  integer,                   intent(in)    :: unit      !< Logical unit.
  character(kind=CK, len=*), intent(in)    :: iotype    !< Edit descriptor.
  integer,                   intent(in)    :: v_list(:) !< Edit descriptor list.
  integer,                   intent(out)   :: iostat    !< IO status code.
  character(kind=CK, len=*), intent(inout) :: iomsg     !< IO status message.

  if (allocated(dtv%raw)) then
    write(unit, "(A)", iostat=iostat, iomsg=iomsg)dtv%raw
  else
    write(unit, "(A)", iostat=iostat, iomsg=iomsg)''
  endif
  endsubroutine write_formatted

  subroutine read_unformatted(dtv, unit, iostat, iomsg)
  !< Unformatted input.
  !<
  !< @bug Change temporary acks: find a more precise length of the input string and avoid the trimming!
  class(string),             intent(inout) :: dtv       !< The string.
  integer,                   intent(in)    :: unit      !< Logical unit.
  integer,                   intent(out)   :: iostat    !< IO status code.
  character(kind=CK, len=*), intent(inout) :: iomsg     !< IO status message.
  character(kind=CK, len=100)              :: temporary !< Temporary storage string.

  read(unit, iostat=iostat, iomsg=iomsg)temporary
  dtv%raw = trim(temporary)
  endsubroutine read_unformatted

  subroutine write_unformatted(dtv, unit, iostat, iomsg)
  !< Unformatted output.
  class(string),             intent(in)    :: dtv    !< The string.
  integer,                   intent(in)    :: unit   !< Logical unit.
  integer,                   intent(out)   :: iostat !< IO status code.
  character(kind=CK, len=*), intent(inout) :: iomsg  !< IO status message.

  if (allocated(dtv%raw)) then
    write(unit, iostat=iostat, iomsg=iomsg)dtv%raw
  else
    write(unit, iostat=iostat, iomsg=iomsg)''
  endif
  endsubroutine write_unformatted

  ! miscellanea
  elemental function replace_one_occurrence(self, old, new) result(replaced)
  !< Return a string with the first occurrence of substring old replaced by new.
  class(string),             intent(in)  :: self      !< The string.
  character(kind=CK, len=*), intent(in)  :: old       !< Old substring.
  character(kind=CK, len=*), intent(in)  :: new       !< New substring.
  type(string)                           :: replaced  !< The string with old replaced by new.
  integer                                :: pos       !< Position from which replace old.
#ifdef __GFORTRAN__
  character(kind=CK, len=:), allocatable :: temporary !< Temporary storage, workaround for GNU bug.
#endif

  if (allocated(self%raw)) then
    replaced = self
    pos = index(string=self%raw, substring=old)
    if (pos>0) then
#ifdef __GFORTRAN__
      temporary = self%raw
      if (pos==1) then
        replaced%raw = new//temporary(len(old)+1:)
      else
        replaced%raw = temporary(1:pos-1)//new//temporary(pos+len(old):)
      endif
#else
      if (pos==1) then
        replaced%raw = new//self%raw(len(old)+1:)
      else
        replaced%raw = self%raw(1:pos-1)//new//self%raw(pos+len(old):)
      endif
#endif
    endif
  endif
  endfunction replace_one_occurrence

  ! non type-bound-procedures
  subroutine get_delimiter_mode(unit, delim, iostat, iomsg)
  !< Get the DELIM changeable connection mode for the given unit.
  !<
  !< If the unit is connected to an internal file, then the default value of NONE is always returned.
  use, intrinsic :: iso_fortran_env, only : iostat_inquire_internal_unit
  integer,                   intent(in)    :: unit         !< The unit for the connection.
  character(len=1, kind=CK), intent(out)   :: delim        !< Represents the value of the DELIM mode.
  integer,                   intent(out)   :: iostat       !< IOSTAT error code, non-zero on error.
  character(*),              intent(inout) :: iomsg        !< IOMSG explanatory message - only defined if iostat is non-zero.
  character(10)                            :: delim_buffer !< Buffer for INQUIRE about DELIM, sized for APOSTROHPE.
  character(len(iomsg))                    :: local_iomsg  !< Local variant of iomsg, so it doesn't get inappropriately redefined.

  ! get the string representation of the changeable mode
  inquire(unit, delim=delim_buffer, iostat=iostat, iomsg=local_iomsg)
  if (iostat == iostat_inquire_internal_unit) then
    ! no way of determining the DELIM mode for an internal file
    iostat = 0
    delim = ''
    return
  elseif (iostat /= 0) then
    iomsg = local_iomsg
    return
  endif
  ! interpret the DELIM string
  if (delim_buffer == 'QUOTE') then
    delim = '"'
  elseif (delim_buffer == 'APOSTROPHE') then
    delim = ''''
  else
    delim = '"'
  endif
  endsubroutine get_delimiter_mode

  subroutine get_next_non_blank_character_this_record(unit, ch, iostat, iomsg)
  !< Get the next non-blank character in the current record.
  integer,                   intent(in)    :: unit   !< Logical unit.
  character(kind=CK, len=1), intent(out)   :: ch     !< The non-blank character read. Not valid if IOSTAT is non-zero.
  integer,                   intent(out)   :: iostat !< IO status code.
  character(kind=CK, len=*), intent(inout) :: iomsg  !< IO status message.

  do
    ! we spcify non-advancing, just in case we want this callable outside the context of a child input statement
    ! the PAD specifier simply saves the need for the READ statement to define ch if EOR is hit
    ! read(unit, "(A)", iostat=iostat, iomsg=iomsg, advance='NO') ch
    ! ...but that causes ifort to blow up at runtime
    read(unit, "(A)", iostat=iostat, iomsg=iomsg, pad='NO') ch
    if (iostat /= 0) return
    if (ch /= '') exit
  enddo
  endsubroutine get_next_non_blank_character_this_record

  subroutine get_next_non_blank_character_any_record(unit, ch, iostat, iomsg)
  !< Get the next non-blank character, advancing records if necessary.
  integer,                   intent(in)    :: unit        !< Logical unit.
  character(kind=CK, len=1), intent(out)   :: ch          !< The non-blank character read. Not valid if IOSTAT is non-zero.
  integer,                   intent(out)   :: iostat      !< IO status code.
  character(kind=CK, len=*), intent(inout) :: iomsg       !< IO status message.
  character(len(iomsg))                    :: local_iomsg !< Local variant of iomsg, so it doesn't get inappropriately redefined.

  do
    call get_next_non_blank_character_this_record(unit=unit, ch=ch, iostat=iostat, iomsg=local_iomsg)
    if (is_iostat_eor(iostat)) then
      ! try again on the next record
      read (unit, "(/)", iostat=iostat, iomsg=iomsg)
      if (iostat /= 0) return
    elseif (iostat /= 0) then
      ! some sort of problem
      iomsg = local_iomsg
      return
    else
      ! got it
      exit
    endif
  enddo
  endsubroutine get_next_non_blank_character_any_record

  subroutine get_decimal_mode(unit, decimal_point, iostat, iomsg)
  !< Get the DECIMAL changeable connection mode for the given unit.
  !<
  !< If the unit is connected to an internal file, then the default value of DECIMAL is always returned. This may not be the
  !< actual value in force at the time of the call to this procedure.
  use, intrinsic :: iso_fortran_env, only : iostat_inquire_internal_unit
  integer,                   intent(in)    :: unit           !< Logical unit.
  logical,                   intent(out)   :: decimal_point  !< True if the decimal mode is POINT, false otherwise.
  integer,                   intent(out)   :: iostat         !< IO status code.
  character(kind=CK, len=*), intent(inout) :: iomsg          !< IO status message.
  character(5)                             :: decimal_buffer !< Buffer for INQUIRE about DECIMAL, sized for POINT or COMMA.
  character(len(iomsg))                    :: local_iomsg    !< Local variant of iomsg, so it doesn't get inappropriately redefined.

  inquire(unit, decimal=decimal_buffer, iostat=iostat, iomsg=local_iomsg)
  if (iostat == iostat_inquire_internal_unit) then
    ! no way of determining the decimal mode for an internal file
    iostat = 0
    decimal_point = .true.
    return
  else if (iostat /= 0) then
    iomsg = local_iomsg
    return
  endif
  decimal_point = decimal_buffer == 'POINT'
  endsubroutine get_decimal_mode
endmodule stringifor_string_t
